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.
Files changed (46) hide show
  1. netbox_map/__init__.py +23 -0
  2. netbox_map/api/__init__.py +0 -0
  3. netbox_map/api/serializers.py +115 -0
  4. netbox_map/api/urls.py +10 -0
  5. netbox_map/api/views.py +30 -0
  6. netbox_map/choices.py +44 -0
  7. netbox_map/filtersets.py +90 -0
  8. netbox_map/forms.py +449 -0
  9. netbox_map/migrations/0001_initial.py +133 -0
  10. netbox_map/migrations/0002_rename_netbox_map_assigne_idx_netbox_map__assigne_eb5b30_idx.py +18 -0
  11. netbox_map/migrations/0003_floorplantile_latitude_longitude_locationcoordinates.py +76 -0
  12. netbox_map/migrations/0004_mapmarker.py +125 -0
  13. netbox_map/migrations/0005_rename_netbox_map__assigne_mapmarker_idx_netbox_map__assigne_6fd3ec_idx.py +18 -0
  14. netbox_map/migrations/0006_mapsettings.py +28 -0
  15. netbox_map/migrations/0007_alter_mapsettings_id.py +18 -0
  16. netbox_map/migrations/__init__.py +0 -0
  17. netbox_map/models.py +464 -0
  18. netbox_map/navigation.py +67 -0
  19. netbox_map/search.py +29 -0
  20. netbox_map/signals.py +150 -0
  21. netbox_map/static/netbox_map/css/floorplan.css +706 -0
  22. netbox_map/static/netbox_map/css/leaflet-1.9.4.min.css +661 -0
  23. netbox_map/static/netbox_map/css/site_map.css +434 -0
  24. netbox_map/static/netbox_map/js/floorplan_editor.js +644 -0
  25. netbox_map/static/netbox_map/js/floorplan_viewer.js +1713 -0
  26. netbox_map/static/netbox_map/js/jspdf.umd.min.js +398 -0
  27. netbox_map/static/netbox_map/js/leaflet-1.9.4.min.js +6 -0
  28. netbox_map/static/netbox_map/js/site_map.js +1354 -0
  29. netbox_map/tables.py +135 -0
  30. netbox_map/template_content.py +41 -0
  31. netbox_map/templates/netbox_map/device_map_locations.html +62 -0
  32. netbox_map/templates/netbox_map/floorplan.html +74 -0
  33. netbox_map/templates/netbox_map/floorplan_visualization.html +560 -0
  34. netbox_map/templates/netbox_map/floorplantile.html +65 -0
  35. netbox_map/templates/netbox_map/inc/device_floorplan_panel.html +25 -0
  36. netbox_map/templates/netbox_map/inc/site_floorplan_panel.html +28 -0
  37. netbox_map/templates/netbox_map/mapmarker.html +82 -0
  38. netbox_map/templates/netbox_map/settings.html +135 -0
  39. netbox_map/templates/netbox_map/site_map.html +121 -0
  40. netbox_map/urls.py +43 -0
  41. netbox_map/views.py +586 -0
  42. netbox_map-0.1.0.dist-info/METADATA +418 -0
  43. netbox_map-0.1.0.dist-info/RECORD +46 -0
  44. netbox_map-0.1.0.dist-info/WHEEL +5 -0
  45. netbox_map-0.1.0.dist-info/licenses/LICENSE +190 -0
  46. 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
+ ]