netbox-map 0.1.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_map/__init__.py +23 -0
- netbox_map/api/__init__.py +0 -0
- netbox_map/api/serializers.py +115 -0
- netbox_map/api/urls.py +10 -0
- netbox_map/api/views.py +30 -0
- netbox_map/choices.py +44 -0
- netbox_map/filtersets.py +90 -0
- netbox_map/forms.py +449 -0
- netbox_map/migrations/0001_initial.py +133 -0
- netbox_map/migrations/0002_rename_netbox_map_assigne_idx_netbox_map__assigne_eb5b30_idx.py +18 -0
- netbox_map/migrations/0003_floorplantile_latitude_longitude_locationcoordinates.py +76 -0
- netbox_map/migrations/0004_mapmarker.py +125 -0
- netbox_map/migrations/0005_rename_netbox_map__assigne_mapmarker_idx_netbox_map__assigne_6fd3ec_idx.py +18 -0
- netbox_map/migrations/0006_mapsettings.py +28 -0
- netbox_map/migrations/0007_alter_mapsettings_id.py +18 -0
- netbox_map/migrations/__init__.py +0 -0
- netbox_map/models.py +464 -0
- netbox_map/navigation.py +67 -0
- netbox_map/search.py +29 -0
- netbox_map/signals.py +150 -0
- netbox_map/static/netbox_map/css/floorplan.css +706 -0
- netbox_map/static/netbox_map/css/leaflet-1.9.4.min.css +661 -0
- netbox_map/static/netbox_map/css/site_map.css +434 -0
- netbox_map/static/netbox_map/js/floorplan_editor.js +644 -0
- netbox_map/static/netbox_map/js/floorplan_viewer.js +1713 -0
- netbox_map/static/netbox_map/js/jspdf.umd.min.js +398 -0
- netbox_map/static/netbox_map/js/leaflet-1.9.4.min.js +6 -0
- netbox_map/static/netbox_map/js/site_map.js +1354 -0
- netbox_map/tables.py +135 -0
- netbox_map/template_content.py +41 -0
- netbox_map/templates/netbox_map/device_map_locations.html +62 -0
- netbox_map/templates/netbox_map/floorplan.html +74 -0
- netbox_map/templates/netbox_map/floorplan_visualization.html +560 -0
- netbox_map/templates/netbox_map/floorplantile.html +65 -0
- netbox_map/templates/netbox_map/inc/device_floorplan_panel.html +25 -0
- netbox_map/templates/netbox_map/inc/site_floorplan_panel.html +28 -0
- netbox_map/templates/netbox_map/mapmarker.html +82 -0
- netbox_map/templates/netbox_map/settings.html +135 -0
- netbox_map/templates/netbox_map/site_map.html +121 -0
- netbox_map/urls.py +43 -0
- netbox_map/views.py +586 -0
- netbox_map-0.1.0.dist-info/METADATA +418 -0
- netbox_map-0.1.0.dist-info/RECORD +46 -0
- netbox_map-0.1.0.dist-info/WHEEL +5 -0
- netbox_map-0.1.0.dist-info/licenses/LICENSE +190 -0
- netbox_map-0.1.0.dist-info/top_level.txt +1 -0
netbox_map/forms.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.contrib.contenttypes.models import ContentType
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
|
|
5
|
+
from dcim.models import Site, Location, Rack, Device, PowerPanel, PowerFeed
|
|
6
|
+
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelBulkEditForm
|
|
7
|
+
from utilities.forms.fields import (
|
|
8
|
+
ContentTypeChoiceField,
|
|
9
|
+
DynamicModelChoiceField,
|
|
10
|
+
CommentField,
|
|
11
|
+
)
|
|
12
|
+
from utilities.forms.rendering import FieldSet
|
|
13
|
+
from .models import FloorPlan, FloorPlanTile, MapMarker, MapSettings, ASSIGNABLE_MODELS
|
|
14
|
+
from .choices import FloorPlanTileTypeChoices, FloorPlanTileStatusChoices
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_assignable_content_types():
|
|
18
|
+
"""Return a queryset of ContentTypes for assignable models."""
|
|
19
|
+
return ContentType.objects.filter(
|
|
20
|
+
app_label='dcim',
|
|
21
|
+
model__in=['device', 'rack', 'powerpanel', 'powerfeed'],
|
|
22
|
+
).order_by('model')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# FloorPlan forms
|
|
27
|
+
#
|
|
28
|
+
|
|
29
|
+
class FloorPlanForm(NetBoxModelForm):
|
|
30
|
+
site = DynamicModelChoiceField(
|
|
31
|
+
label=_('Site'),
|
|
32
|
+
queryset=Site.objects.all(),
|
|
33
|
+
)
|
|
34
|
+
location = DynamicModelChoiceField(
|
|
35
|
+
label=_('Location'),
|
|
36
|
+
queryset=Location.objects.all(),
|
|
37
|
+
required=False,
|
|
38
|
+
query_params={
|
|
39
|
+
'site_id': '$site'
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
comments = CommentField()
|
|
43
|
+
|
|
44
|
+
fieldsets = (
|
|
45
|
+
FieldSet(
|
|
46
|
+
'site', 'location', 'name', 'description', 'tags',
|
|
47
|
+
name=_('Floor Plan')
|
|
48
|
+
),
|
|
49
|
+
FieldSet(
|
|
50
|
+
'grid_width', 'grid_height', 'tile_size',
|
|
51
|
+
name=_('Grid Configuration')
|
|
52
|
+
),
|
|
53
|
+
FieldSet(
|
|
54
|
+
'background_image',
|
|
55
|
+
name=_('Background')
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
class Meta:
|
|
60
|
+
model = FloorPlan
|
|
61
|
+
fields = [
|
|
62
|
+
'site', 'location', 'name', 'grid_width', 'grid_height',
|
|
63
|
+
'tile_size', 'background_image', 'description', 'comments', 'tags',
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FloorPlanFilterForm(NetBoxModelFilterSetForm):
|
|
68
|
+
model = FloorPlan
|
|
69
|
+
site_id = DynamicModelChoiceField(
|
|
70
|
+
queryset=Site.objects.all(),
|
|
71
|
+
required=False,
|
|
72
|
+
label=_('Site')
|
|
73
|
+
)
|
|
74
|
+
location_id = DynamicModelChoiceField(
|
|
75
|
+
queryset=Location.objects.all(),
|
|
76
|
+
required=False,
|
|
77
|
+
label=_('Location'),
|
|
78
|
+
query_params={
|
|
79
|
+
'site_id': '$site_id'
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
fieldsets = (
|
|
84
|
+
FieldSet('site_id', 'location_id'),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FloorPlanBulkEditForm(NetBoxModelBulkEditForm):
|
|
89
|
+
grid_width = forms.IntegerField(required=False)
|
|
90
|
+
grid_height = forms.IntegerField(required=False)
|
|
91
|
+
tile_size = forms.IntegerField(required=False)
|
|
92
|
+
description = forms.CharField(max_length=200, required=False)
|
|
93
|
+
|
|
94
|
+
model = FloorPlan
|
|
95
|
+
nullable_fields = ('location', 'description', 'background_image')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# FloorPlanTile forms
|
|
100
|
+
#
|
|
101
|
+
|
|
102
|
+
class FloorPlanTileForm(NetBoxModelForm):
|
|
103
|
+
floorplan = DynamicModelChoiceField(
|
|
104
|
+
label=_('Floor Plan'),
|
|
105
|
+
queryset=FloorPlan.objects.all()
|
|
106
|
+
)
|
|
107
|
+
assigned_object_type = ContentTypeChoiceField(
|
|
108
|
+
label=_('Object Type'),
|
|
109
|
+
queryset=get_assignable_content_types(),
|
|
110
|
+
required=False,
|
|
111
|
+
help_text=_('Select the type of object to assign (Rack, Device, etc.)')
|
|
112
|
+
)
|
|
113
|
+
rack = DynamicModelChoiceField(
|
|
114
|
+
label=_('Rack'),
|
|
115
|
+
queryset=Rack.objects.all(),
|
|
116
|
+
required=False,
|
|
117
|
+
query_params={
|
|
118
|
+
'site_id': '$floorplan',
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
device = DynamicModelChoiceField(
|
|
122
|
+
label=_('Device'),
|
|
123
|
+
queryset=Device.objects.all(),
|
|
124
|
+
required=False,
|
|
125
|
+
query_params={
|
|
126
|
+
'site_id': '$floorplan',
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
powerpanel = DynamicModelChoiceField(
|
|
130
|
+
label=_('Power Panel'),
|
|
131
|
+
queryset=PowerPanel.objects.all(),
|
|
132
|
+
required=False,
|
|
133
|
+
query_params={
|
|
134
|
+
'site_id': '$floorplan',
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
powerfeed = DynamicModelChoiceField(
|
|
138
|
+
label=_('Power Feed'),
|
|
139
|
+
queryset=PowerFeed.objects.all(),
|
|
140
|
+
required=False,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
fieldsets = (
|
|
144
|
+
FieldSet(
|
|
145
|
+
'floorplan', 'x_position', 'y_position', 'width', 'height',
|
|
146
|
+
'orientation', 'tags',
|
|
147
|
+
name=_('Position')
|
|
148
|
+
),
|
|
149
|
+
FieldSet(
|
|
150
|
+
'tile_type', 'status', 'label',
|
|
151
|
+
name=_('Tile')
|
|
152
|
+
),
|
|
153
|
+
FieldSet(
|
|
154
|
+
'fov_direction', 'fov_angle', 'fov_distance',
|
|
155
|
+
name=_('Camera FOV Settings')
|
|
156
|
+
),
|
|
157
|
+
FieldSet(
|
|
158
|
+
'assigned_object_type', 'rack', 'device', 'powerpanel', 'powerfeed',
|
|
159
|
+
name=_('Assigned Object')
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
class Meta:
|
|
164
|
+
model = FloorPlanTile
|
|
165
|
+
fields = [
|
|
166
|
+
'floorplan', 'x_position', 'y_position', 'width', 'height',
|
|
167
|
+
'label', 'tile_type', 'status', 'orientation',
|
|
168
|
+
'fov_direction', 'fov_angle', 'fov_distance',
|
|
169
|
+
'assigned_object_type', 'tags',
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
def __init__(self, *args, **kwargs):
|
|
173
|
+
super().__init__(*args, **kwargs)
|
|
174
|
+
|
|
175
|
+
# Pre-populate the object selector if editing an existing tile
|
|
176
|
+
if self.instance.pk and self.instance.assigned_object_type:
|
|
177
|
+
model_name = self.instance.assigned_object_type.model
|
|
178
|
+
if model_name in ('rack', 'device', 'powerpanel', 'powerfeed'):
|
|
179
|
+
field = self.fields.get(model_name)
|
|
180
|
+
if field and self.instance.assigned_object_id:
|
|
181
|
+
field.initial = self.instance.assigned_object_id
|
|
182
|
+
|
|
183
|
+
def clean(self):
|
|
184
|
+
super().clean()
|
|
185
|
+
|
|
186
|
+
assigned_object_type = self.cleaned_data.get('assigned_object_type')
|
|
187
|
+
|
|
188
|
+
if assigned_object_type:
|
|
189
|
+
model_name = assigned_object_type.model
|
|
190
|
+
obj = self.cleaned_data.get(model_name)
|
|
191
|
+
if obj:
|
|
192
|
+
self.cleaned_data['assigned_object_id'] = obj.pk
|
|
193
|
+
else:
|
|
194
|
+
self.cleaned_data['assigned_object_id'] = None
|
|
195
|
+
self.cleaned_data['assigned_object_type'] = None
|
|
196
|
+
else:
|
|
197
|
+
self.cleaned_data['assigned_object_type'] = None
|
|
198
|
+
self.cleaned_data['assigned_object_id'] = None
|
|
199
|
+
|
|
200
|
+
return self.cleaned_data
|
|
201
|
+
|
|
202
|
+
def save(self, *args, **kwargs):
|
|
203
|
+
# Set the assigned_object_id from the cleaned data
|
|
204
|
+
self.instance.assigned_object_type = self.cleaned_data.get('assigned_object_type')
|
|
205
|
+
self.instance.assigned_object_id = self.cleaned_data.get('assigned_object_id')
|
|
206
|
+
return super().save(*args, **kwargs)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class FloorPlanTileFilterForm(NetBoxModelFilterSetForm):
|
|
210
|
+
model = FloorPlanTile
|
|
211
|
+
floorplan_id = DynamicModelChoiceField(
|
|
212
|
+
queryset=FloorPlan.objects.all(),
|
|
213
|
+
required=False,
|
|
214
|
+
label=_('Floor Plan')
|
|
215
|
+
)
|
|
216
|
+
assigned_object_type = ContentTypeChoiceField(
|
|
217
|
+
queryset=get_assignable_content_types(),
|
|
218
|
+
required=False,
|
|
219
|
+
label=_('Object Type')
|
|
220
|
+
)
|
|
221
|
+
tile_type = forms.MultipleChoiceField(
|
|
222
|
+
choices=FloorPlanTileTypeChoices,
|
|
223
|
+
required=False,
|
|
224
|
+
label=_('Tile Type')
|
|
225
|
+
)
|
|
226
|
+
status = forms.MultipleChoiceField(
|
|
227
|
+
choices=FloorPlanTileStatusChoices,
|
|
228
|
+
required=False,
|
|
229
|
+
label=_('Status')
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
fieldsets = (
|
|
233
|
+
FieldSet('floorplan_id', 'assigned_object_type', 'tile_type', 'status'),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
#
|
|
238
|
+
# MapMarker forms
|
|
239
|
+
#
|
|
240
|
+
|
|
241
|
+
class MapMarkerForm(NetBoxModelForm):
|
|
242
|
+
site = DynamicModelChoiceField(
|
|
243
|
+
label=_('Site'),
|
|
244
|
+
queryset=Site.objects.all(),
|
|
245
|
+
required=False,
|
|
246
|
+
)
|
|
247
|
+
assigned_object_type = ContentTypeChoiceField(
|
|
248
|
+
label=_('Object Type'),
|
|
249
|
+
queryset=get_assignable_content_types(),
|
|
250
|
+
required=False,
|
|
251
|
+
help_text=_('Select the type of object to assign')
|
|
252
|
+
)
|
|
253
|
+
rack = DynamicModelChoiceField(
|
|
254
|
+
label=_('Rack'),
|
|
255
|
+
queryset=Rack.objects.all(),
|
|
256
|
+
required=False,
|
|
257
|
+
query_params={'site_id': '$site'},
|
|
258
|
+
)
|
|
259
|
+
device = DynamicModelChoiceField(
|
|
260
|
+
label=_('Device'),
|
|
261
|
+
queryset=Device.objects.all(),
|
|
262
|
+
required=False,
|
|
263
|
+
query_params={'site_id': '$site'},
|
|
264
|
+
)
|
|
265
|
+
powerpanel = DynamicModelChoiceField(
|
|
266
|
+
label=_('Power Panel'),
|
|
267
|
+
queryset=PowerPanel.objects.all(),
|
|
268
|
+
required=False,
|
|
269
|
+
query_params={'site_id': '$site'},
|
|
270
|
+
)
|
|
271
|
+
powerfeed = DynamicModelChoiceField(
|
|
272
|
+
label=_('Power Feed'),
|
|
273
|
+
queryset=PowerFeed.objects.all(),
|
|
274
|
+
required=False,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
fieldsets = (
|
|
278
|
+
FieldSet(
|
|
279
|
+
'latitude', 'longitude', 'label', 'marker_type', 'status', 'site', 'description', 'tags',
|
|
280
|
+
name=_('Map Marker')
|
|
281
|
+
),
|
|
282
|
+
FieldSet(
|
|
283
|
+
'fov_direction', 'fov_angle', 'fov_distance',
|
|
284
|
+
name=_('Camera FOV Settings')
|
|
285
|
+
),
|
|
286
|
+
FieldSet(
|
|
287
|
+
'assigned_object_type', 'rack', 'device', 'powerpanel', 'powerfeed',
|
|
288
|
+
name=_('Assigned Object')
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
class Meta:
|
|
293
|
+
model = MapMarker
|
|
294
|
+
fields = [
|
|
295
|
+
'latitude', 'longitude', 'label', 'marker_type', 'status', 'site',
|
|
296
|
+
'fov_direction', 'fov_angle', 'fov_distance',
|
|
297
|
+
'assigned_object_type', 'description', 'tags',
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
def __init__(self, *args, **kwargs):
|
|
301
|
+
super().__init__(*args, **kwargs)
|
|
302
|
+
if self.instance.pk and self.instance.assigned_object_type:
|
|
303
|
+
model_name = self.instance.assigned_object_type.model
|
|
304
|
+
if model_name in ('rack', 'device', 'powerpanel', 'powerfeed'):
|
|
305
|
+
field = self.fields.get(model_name)
|
|
306
|
+
if field and self.instance.assigned_object_id:
|
|
307
|
+
field.initial = self.instance.assigned_object_id
|
|
308
|
+
|
|
309
|
+
def clean(self):
|
|
310
|
+
super().clean()
|
|
311
|
+
assigned_object_type = self.cleaned_data.get('assigned_object_type')
|
|
312
|
+
if assigned_object_type:
|
|
313
|
+
model_name = assigned_object_type.model
|
|
314
|
+
obj = self.cleaned_data.get(model_name)
|
|
315
|
+
if obj:
|
|
316
|
+
self.instance.assigned_object_id = obj.pk
|
|
317
|
+
else:
|
|
318
|
+
self.instance.assigned_object_id = None
|
|
319
|
+
self.cleaned_data['assigned_object_type'] = None
|
|
320
|
+
else:
|
|
321
|
+
self.cleaned_data['assigned_object_type'] = None
|
|
322
|
+
self.instance.assigned_object_id = None
|
|
323
|
+
return self.cleaned_data
|
|
324
|
+
|
|
325
|
+
def save(self, *args, **kwargs):
|
|
326
|
+
self.instance.assigned_object_type = self.cleaned_data.get('assigned_object_type')
|
|
327
|
+
return super().save(*args, **kwargs)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
#
|
|
331
|
+
# MapSettings form
|
|
332
|
+
#
|
|
333
|
+
|
|
334
|
+
DEVICE_FIELD_CHOICES = [
|
|
335
|
+
('status', _('Status')),
|
|
336
|
+
('role', _('Role')),
|
|
337
|
+
('device_type', _('Device Type')),
|
|
338
|
+
('platform', _('Platform')),
|
|
339
|
+
('serial', _('Serial')),
|
|
340
|
+
('asset_tag', _('Asset Tag')),
|
|
341
|
+
('tenant', _('Tenant')),
|
|
342
|
+
('site', _('Site')),
|
|
343
|
+
('location', _('Location')),
|
|
344
|
+
('rack', _('Rack')),
|
|
345
|
+
('position', _('Position')),
|
|
346
|
+
('face', _('Face')),
|
|
347
|
+
('airflow', _('Airflow')),
|
|
348
|
+
('primary_ip4', _('Primary IPv4')),
|
|
349
|
+
('primary_ip6', _('Primary IPv6')),
|
|
350
|
+
('oob_ip', _('OOB IP')),
|
|
351
|
+
('cluster', _('Cluster')),
|
|
352
|
+
('virtual_chassis', _('Virtual Chassis')),
|
|
353
|
+
('vc_position', _('VC Position')),
|
|
354
|
+
('description', _('Description')),
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
RACK_FIELD_CHOICES = [
|
|
358
|
+
('status', _('Status')),
|
|
359
|
+
('role', _('Role')),
|
|
360
|
+
('facility_id', _('Facility ID')),
|
|
361
|
+
('serial', _('Serial')),
|
|
362
|
+
('asset_tag', _('Asset Tag')),
|
|
363
|
+
('u_height', _('U Height')),
|
|
364
|
+
('width', _('Width')),
|
|
365
|
+
('type', _('Type')),
|
|
366
|
+
('weight', _('Weight')),
|
|
367
|
+
('max_weight', _('Max Weight')),
|
|
368
|
+
('tenant', _('Tenant')),
|
|
369
|
+
('site', _('Site')),
|
|
370
|
+
('location', _('Location')),
|
|
371
|
+
('description', _('Description')),
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
POWERPANEL_FIELD_CHOICES = [
|
|
375
|
+
('site', _('Site')),
|
|
376
|
+
('location', _('Location')),
|
|
377
|
+
('description', _('Description')),
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
POWERFEED_FIELD_CHOICES = [
|
|
381
|
+
('status', _('Status')),
|
|
382
|
+
('type', _('Type')),
|
|
383
|
+
('supply', _('Supply')),
|
|
384
|
+
('voltage', _('Voltage')),
|
|
385
|
+
('amperage', _('Amperage')),
|
|
386
|
+
('max_utilization', _('Max Utilization')),
|
|
387
|
+
('power_panel', _('Power Panel')),
|
|
388
|
+
('rack', _('Rack')),
|
|
389
|
+
('description', _('Description')),
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class MapSettingsForm(forms.ModelForm):
|
|
394
|
+
"""Form for editing map detail panel settings."""
|
|
395
|
+
|
|
396
|
+
device_fields = forms.MultipleChoiceField(
|
|
397
|
+
choices=DEVICE_FIELD_CHOICES,
|
|
398
|
+
widget=forms.CheckboxSelectMultiple,
|
|
399
|
+
required=False,
|
|
400
|
+
label=_('Device Fields'),
|
|
401
|
+
)
|
|
402
|
+
rack_fields = forms.MultipleChoiceField(
|
|
403
|
+
choices=RACK_FIELD_CHOICES,
|
|
404
|
+
widget=forms.CheckboxSelectMultiple,
|
|
405
|
+
required=False,
|
|
406
|
+
label=_('Rack Fields'),
|
|
407
|
+
)
|
|
408
|
+
powerpanel_fields = forms.MultipleChoiceField(
|
|
409
|
+
choices=POWERPANEL_FIELD_CHOICES,
|
|
410
|
+
widget=forms.CheckboxSelectMultiple,
|
|
411
|
+
required=False,
|
|
412
|
+
label=_('Power Panel Fields'),
|
|
413
|
+
)
|
|
414
|
+
powerfeed_fields = forms.MultipleChoiceField(
|
|
415
|
+
choices=POWERFEED_FIELD_CHOICES,
|
|
416
|
+
widget=forms.CheckboxSelectMultiple,
|
|
417
|
+
required=False,
|
|
418
|
+
label=_('Power Feed Fields'),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
class Meta:
|
|
422
|
+
model = MapSettings
|
|
423
|
+
fields = (
|
|
424
|
+
'show_mac', 'show_custom_fields', 'sync_device_gps',
|
|
425
|
+
'device_fields', 'rack_fields', 'powerpanel_fields', 'powerfeed_fields',
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class MapMarkerFilterForm(NetBoxModelFilterSetForm):
|
|
430
|
+
model = MapMarker
|
|
431
|
+
site_id = DynamicModelChoiceField(
|
|
432
|
+
queryset=Site.objects.all(),
|
|
433
|
+
required=False,
|
|
434
|
+
label=_('Site')
|
|
435
|
+
)
|
|
436
|
+
marker_type = forms.MultipleChoiceField(
|
|
437
|
+
choices=FloorPlanTileTypeChoices,
|
|
438
|
+
required=False,
|
|
439
|
+
label=_('Marker Type')
|
|
440
|
+
)
|
|
441
|
+
status = forms.MultipleChoiceField(
|
|
442
|
+
choices=FloorPlanTileStatusChoices,
|
|
443
|
+
required=False,
|
|
444
|
+
label=_('Status')
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
fieldsets = (
|
|
448
|
+
FieldSet('site_id', 'marker_type', 'status'),
|
|
449
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import django.core.validators
|
|
2
|
+
import django.db.models.deletion
|
|
3
|
+
import netbox.models.deletion
|
|
4
|
+
import taggit.managers
|
|
5
|
+
import utilities.json
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
('contenttypes', '0002_remove_content_type_name'),
|
|
15
|
+
('dcim', '0225_gfk_indexes'),
|
|
16
|
+
('extras', '0134_owner'),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
operations = [
|
|
20
|
+
migrations.CreateModel(
|
|
21
|
+
name='FloorPlan',
|
|
22
|
+
fields=[
|
|
23
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
24
|
+
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
|
25
|
+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
|
26
|
+
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
|
27
|
+
('name', models.CharField(max_length=200, verbose_name='name')),
|
|
28
|
+
('grid_width', models.PositiveIntegerField(
|
|
29
|
+
default=20,
|
|
30
|
+
help_text='Width of the grid in tiles',
|
|
31
|
+
validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000)],
|
|
32
|
+
verbose_name='grid width',
|
|
33
|
+
)),
|
|
34
|
+
('grid_height', models.PositiveIntegerField(
|
|
35
|
+
default=20,
|
|
36
|
+
help_text='Height of the grid in tiles',
|
|
37
|
+
validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000)],
|
|
38
|
+
verbose_name='grid height',
|
|
39
|
+
)),
|
|
40
|
+
('tile_size', models.PositiveIntegerField(
|
|
41
|
+
default=60,
|
|
42
|
+
help_text='Size of each tile in pixels for rendering',
|
|
43
|
+
validators=[django.core.validators.MinValueValidator(5), django.core.validators.MaxValueValidator(200)],
|
|
44
|
+
verbose_name='tile size',
|
|
45
|
+
)),
|
|
46
|
+
('background_image', models.ImageField(blank=True, null=True, upload_to='floorplan-backgrounds/', verbose_name='background image')),
|
|
47
|
+
('description', models.CharField(blank=True, max_length=200, verbose_name='description')),
|
|
48
|
+
('comments', models.TextField(blank=True, verbose_name='comments')),
|
|
49
|
+
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='floorplans', to='dcim.location')),
|
|
50
|
+
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='floorplans', to='dcim.site')),
|
|
51
|
+
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
|
52
|
+
],
|
|
53
|
+
options={
|
|
54
|
+
'verbose_name': 'floor plan',
|
|
55
|
+
'verbose_name_plural': 'floor plans',
|
|
56
|
+
'ordering': ('site', 'name'),
|
|
57
|
+
},
|
|
58
|
+
bases=(netbox.models.deletion.DeleteMixin, models.Model),
|
|
59
|
+
),
|
|
60
|
+
migrations.CreateModel(
|
|
61
|
+
name='FloorPlanTile',
|
|
62
|
+
fields=[
|
|
63
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
64
|
+
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
|
65
|
+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
|
66
|
+
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
|
67
|
+
('x_position', models.PositiveIntegerField(help_text='X coordinate on the grid (0-indexed)', verbose_name='X position')),
|
|
68
|
+
('y_position', models.PositiveIntegerField(help_text='Y coordinate on the grid (0-indexed)', verbose_name='Y position')),
|
|
69
|
+
('width', models.PositiveIntegerField(
|
|
70
|
+
default=1,
|
|
71
|
+
help_text='Width in grid cells',
|
|
72
|
+
validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)],
|
|
73
|
+
verbose_name='width',
|
|
74
|
+
)),
|
|
75
|
+
('height', models.PositiveIntegerField(
|
|
76
|
+
default=1,
|
|
77
|
+
help_text='Height in grid cells',
|
|
78
|
+
validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)],
|
|
79
|
+
verbose_name='height',
|
|
80
|
+
)),
|
|
81
|
+
('assigned_object_type', models.ForeignKey(
|
|
82
|
+
blank=True, null=True,
|
|
83
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
84
|
+
related_name='+',
|
|
85
|
+
to='contenttypes.contenttype',
|
|
86
|
+
help_text='Type of assigned object',
|
|
87
|
+
)),
|
|
88
|
+
('assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True, help_text='ID of assigned object')),
|
|
89
|
+
('label', models.CharField(blank=True, max_length=100, help_text='Custom label (overrides assigned object name)', verbose_name='label')),
|
|
90
|
+
('tile_type', models.CharField(default='rack', max_length=50, verbose_name='tile type')),
|
|
91
|
+
('status', models.CharField(default='active', max_length=50, verbose_name='status')),
|
|
92
|
+
('orientation', models.PositiveSmallIntegerField(default=0, help_text='Rotation in degrees (0, 90, 180, 270)', verbose_name='orientation')),
|
|
93
|
+
('fov_direction', models.PositiveSmallIntegerField(
|
|
94
|
+
default=0,
|
|
95
|
+
help_text='Camera viewing direction in degrees (0=north, 90=east, 180=south, 270=west)',
|
|
96
|
+
validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(360)],
|
|
97
|
+
verbose_name='FOV direction',
|
|
98
|
+
)),
|
|
99
|
+
('fov_angle', models.PositiveSmallIntegerField(
|
|
100
|
+
default=90,
|
|
101
|
+
help_text='Camera field of view width in degrees',
|
|
102
|
+
validators=[django.core.validators.MinValueValidator(10), django.core.validators.MaxValueValidator(360)],
|
|
103
|
+
verbose_name='FOV angle',
|
|
104
|
+
)),
|
|
105
|
+
('fov_distance', models.PositiveSmallIntegerField(
|
|
106
|
+
default=5,
|
|
107
|
+
help_text='Camera view distance in grid cells',
|
|
108
|
+
validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)],
|
|
109
|
+
verbose_name='FOV distance',
|
|
110
|
+
)),
|
|
111
|
+
('floorplan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tiles', to='netbox_map.floorplan')),
|
|
112
|
+
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
|
113
|
+
],
|
|
114
|
+
options={
|
|
115
|
+
'verbose_name': 'floor plan tile',
|
|
116
|
+
'verbose_name_plural': 'floor plan tiles',
|
|
117
|
+
'ordering': ('floorplan', 'y_position', 'x_position'),
|
|
118
|
+
},
|
|
119
|
+
bases=(netbox.models.deletion.DeleteMixin, models.Model),
|
|
120
|
+
),
|
|
121
|
+
migrations.AddConstraint(
|
|
122
|
+
model_name='floorplan',
|
|
123
|
+
constraint=models.UniqueConstraint(fields=('site', 'name'), name='netbox_map_floorplan_unique_site_name'),
|
|
124
|
+
),
|
|
125
|
+
migrations.AddConstraint(
|
|
126
|
+
model_name='floorplantile',
|
|
127
|
+
constraint=models.UniqueConstraint(fields=('floorplan', 'x_position', 'y_position'), name='netbox_map_floorplantile_unique_position'),
|
|
128
|
+
),
|
|
129
|
+
migrations.AddIndex(
|
|
130
|
+
model_name='floorplantile',
|
|
131
|
+
index=models.Index(fields=['assigned_object_type', 'assigned_object_id'], name='netbox_map_assigne_idx'),
|
|
132
|
+
),
|
|
133
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 5.2.11 on 2026-02-21 16:38
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('netbox_map', '0001_initial'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RenameIndex(
|
|
14
|
+
model_name='floorplantile',
|
|
15
|
+
new_name='netbox_map__assigne_eb5b30_idx',
|
|
16
|
+
old_name='netbox_map_assigne_idx',
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import django.db.models.deletion
|
|
2
|
+
import netbox.models.deletion
|
|
3
|
+
import taggit.managers
|
|
4
|
+
import utilities.json
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
('dcim', '0225_gfk_indexes'),
|
|
12
|
+
('extras', '0134_owner'),
|
|
13
|
+
('netbox_map', '0002_rename_netbox_map_assigne_idx_netbox_map__assigne_eb5b30_idx'),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
# Add optional lat/lng to FloorPlanTile for global map placement
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='floorplantile',
|
|
20
|
+
name='latitude',
|
|
21
|
+
field=models.DecimalField(
|
|
22
|
+
blank=True,
|
|
23
|
+
decimal_places=6,
|
|
24
|
+
help_text='Latitude for global map placement (-90 to 90)',
|
|
25
|
+
max_digits=8,
|
|
26
|
+
null=True,
|
|
27
|
+
verbose_name='latitude',
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
migrations.AddField(
|
|
31
|
+
model_name='floorplantile',
|
|
32
|
+
name='longitude',
|
|
33
|
+
field=models.DecimalField(
|
|
34
|
+
blank=True,
|
|
35
|
+
decimal_places=6,
|
|
36
|
+
help_text='Longitude for global map placement (-180 to 180)',
|
|
37
|
+
max_digits=9,
|
|
38
|
+
null=True,
|
|
39
|
+
verbose_name='longitude',
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
# New LocationCoordinates model
|
|
43
|
+
migrations.CreateModel(
|
|
44
|
+
name='LocationCoordinates',
|
|
45
|
+
fields=[
|
|
46
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
47
|
+
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
|
48
|
+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
|
49
|
+
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
|
50
|
+
('latitude', models.DecimalField(
|
|
51
|
+
decimal_places=6,
|
|
52
|
+
help_text='Latitude (-90 to 90)',
|
|
53
|
+
max_digits=8,
|
|
54
|
+
verbose_name='latitude',
|
|
55
|
+
)),
|
|
56
|
+
('longitude', models.DecimalField(
|
|
57
|
+
decimal_places=6,
|
|
58
|
+
help_text='Longitude (-180 to 180)',
|
|
59
|
+
max_digits=9,
|
|
60
|
+
verbose_name='longitude',
|
|
61
|
+
)),
|
|
62
|
+
('location', models.OneToOneField(
|
|
63
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
64
|
+
related_name='coordinates',
|
|
65
|
+
to='dcim.location',
|
|
66
|
+
)),
|
|
67
|
+
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
|
68
|
+
],
|
|
69
|
+
options={
|
|
70
|
+
'verbose_name': 'location coordinates',
|
|
71
|
+
'verbose_name_plural': 'location coordinates',
|
|
72
|
+
'ordering': ('location',),
|
|
73
|
+
},
|
|
74
|
+
bases=(netbox.models.deletion.DeleteMixin, models.Model),
|
|
75
|
+
),
|
|
76
|
+
]
|