ipfabric_netbox 4.2.2b3__py3-none-any.whl → 4.3.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.

Potentially problematic release.


This version of ipfabric_netbox might be problematic. Click here for more details.

@@ -6,9 +6,9 @@ class NetboxIPFabricConfig(PluginConfig):
6
6
  name = "ipfabric_netbox"
7
7
  verbose_name = "NetBox IP Fabric SoT Plugin"
8
8
  description = "Sync IP Fabric into NetBox"
9
- version = "4.2.2b3"
9
+ version = "4.3.0"
10
10
  base_url = "ipfabric"
11
- min_version = "4.2.4"
11
+ min_version = "4.4.0"
12
12
 
13
13
  def ready(self):
14
14
  super().ready()
ipfabric_netbox/forms.py CHANGED
@@ -531,13 +531,28 @@ class IPFabricSyncForm(NetBoxModelForm):
531
531
  widgets = {"source": HTMXSelect()}
532
532
 
533
533
  def __init__(self, *args, **kwargs):
534
+ # The initial data for BooleanFields change to list on HTMX requests.
535
+ # This causes URL to bloat, so we need to sanitize it before it's used.
536
+ initial = kwargs.get("initial", {}).copy()
537
+ for name, value in initial.items():
538
+ if (
539
+ (
540
+ name.startswith("ipf_")
541
+ or (
542
+ name in self.base_fields
543
+ and isinstance(self.base_fields[name], forms.BooleanField)
544
+ )
545
+ )
546
+ and isinstance(value, list)
547
+ and len(value) > 1
548
+ ):
549
+ initial[name] = value[-1] # Keep only the last value
550
+ kwargs["initial"] = initial
534
551
  super().__init__(*args, **kwargs)
535
- self.source_type = None
536
- ingestion_type = get_field_value(self, "source")
552
+
553
+ source = get_field_value(self, "source")
537
554
 
538
555
  if not self.data:
539
- if ingestion_type:
540
- self.source_type = IPFabricSource.objects.get(pk=ingestion_type).type
541
556
  if sites := get_field_value(self, "sites"):
542
557
  sites = list_to_choices(str_to_list(sites))
543
558
  self.fields["sites"].choices = sites
@@ -547,46 +562,82 @@ class IPFabricSyncForm(NetBoxModelForm):
547
562
  snapshot_sites = IPFabricSnapshot.objects.get(pk=snapshot_id).sites
548
563
  choices = list_to_choices(str_to_list(snapshot_sites))
549
564
  self.fields["sites"].choices = choices
565
+ source = self.data.get("source")
550
566
 
567
+ # These values are not populated by data on instance, so we need to set them manually
551
568
  if self.instance and self.instance.pk:
552
569
  if not kwargs.get("initial"):
553
- self.source_type = self.instance.snapshot_data.source.type
554
- self.initial["source"] = self.instance.snapshot_data.source
570
+ source = self.instance.snapshot_data.source
555
571
  if not self.data:
556
572
  self.fields["sites"].choices = list_to_choices(
557
573
  self.instance.snapshot_data.sites
558
574
  )
559
-
560
- self.initial["sites"] = self.instance.parameters.get("sites", [])
575
+ else:
576
+ source = kwargs["initial"].get(
577
+ "source", self.instance.snapshot_data.source
578
+ )
579
+ self.initial["source"] = source
580
+ if "groups" not in self.initial:
561
581
  self.initial["groups"] = self.instance.parameters.get("groups", [])
562
582
 
583
+ # Handle sites field initialization
584
+ if "sites" not in self.initial:
585
+ selected_sites = self.instance.parameters.get("sites", [])
586
+ self.initial["sites"] = selected_sites
587
+
588
+ # Ensure the field has the selected sites as both choices and initial values
589
+ if not self.data and selected_sites:
590
+ # Get current choices and ensure selected sites are included
591
+ current_choices = (
592
+ list(self.fields["sites"].choices)
593
+ if hasattr(self.fields["sites"], "choices")
594
+ else []
595
+ )
596
+ current_choice_values = [choice[0] for choice in current_choices]
597
+
598
+ # Add any missing selected sites to choices
599
+ for site in str_to_list(selected_sites):
600
+ if site not in current_choice_values:
601
+ current_choices.append((site, site))
602
+
603
+ self.fields["sites"].choices = current_choices
604
+ self.fields["sites"].initial = selected_sites
605
+ else:
606
+ self.fields["sites"].initial = self.initial["sites"]
607
+
563
608
  now = local_now().strftime("%Y-%m-%d %H:%M:%S")
564
609
  self.fields["scheduled"].help_text += f" (current time: <strong>{now}</strong>)"
565
610
 
566
611
  # Add backend-specific form fields
567
612
  self.backend_fields = {}
568
613
 
614
+ # Prepare buttons for each target Model
569
615
  for k, v in sync_parameters.items():
570
616
  self.backend_fields[k] = []
571
617
  for name, form_field in v.items():
572
618
  field_name = f"ipf_{name}"
573
619
  self.backend_fields[k].append(field_name)
574
- self.fields[field_name] = copy.copy(form_field)
620
+ self.fields[field_name] = copy.deepcopy(form_field)
575
621
  if self.instance and self.instance.parameters:
576
- self.fields[field_name].initial = self.instance.parameters.get(name)
622
+ value = self.instance.parameters.get(name)
623
+ self.fields[field_name].initial = value
577
624
 
578
625
  # Set fieldsets dynamically based and backend_fields
579
626
  fieldsets = [
580
627
  FieldSet("name", "source", "groups", name=_("IP Fabric Source")),
581
628
  ]
582
- if self.source_type == "local":
583
- fieldsets.append(
584
- FieldSet("snapshot_data", "sites", name=_("Snapshot Information")),
585
- )
586
- else:
587
- fieldsets.append(
588
- FieldSet("snapshot_data", name=_("Snapshot Information")),
589
- )
629
+ # Only show snapshot and sites if source is selected
630
+ if source:
631
+ if isinstance(source, str) or isinstance(source, int):
632
+ source = IPFabricSource.objects.get(pk=source)
633
+ if source.type == "local":
634
+ fieldsets.append(
635
+ FieldSet("snapshot_data", "sites", name=_("Snapshot Information")),
636
+ )
637
+ else:
638
+ fieldsets.append(
639
+ FieldSet("snapshot_data", name=_("Snapshot Information")),
640
+ )
590
641
  for k, v in self.backend_fields.items():
591
642
  fieldsets.append(FieldSet(*v, name=f"{k.upper()} Parameters"))
592
643
  fieldsets.append(
@@ -602,10 +653,22 @@ class IPFabricSyncForm(NetBoxModelForm):
602
653
  def clean(self):
603
654
  super().clean()
604
655
 
656
+ source = self.cleaned_data.get("source")
657
+ snapshot = self.cleaned_data.get("snapshot_data")
658
+
659
+ if (
660
+ source
661
+ and snapshot
662
+ and IPFabricSource.objects.get(pk=source.pk)
663
+ != IPFabricSnapshot.objects.get(pk=snapshot.pk).source
664
+ ):
665
+ raise ValidationError(
666
+ {"snapshot_data": _("Snapshot does not belong to the selected source.")}
667
+ )
668
+
605
669
  sites = self.data.get("sites")
606
670
  self.fields["sites"].choices = list_to_choices(str_to_list(sites))
607
671
  if sites and "snapshot_data" in self.cleaned_data:
608
- snapshot = self.cleaned_data["snapshot_data"]
609
672
  # Check if all sites are valid - fail if any site is not found in snapshot.sites
610
673
  if not all(
611
674
  any(site in snapshot_site for snapshot_site in snapshot.sites)
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
29
29
 
30
30
  def create_custom_field(
31
31
  apps: "apps_type",
32
+ db_alias: str,
32
33
  field_name: str,
33
34
  label: str,
34
35
  target_models: list,
@@ -37,6 +38,7 @@ def create_custom_field(
37
38
  ):
38
39
  """Create a single custom field and link it to required models."""
39
40
  ObjectType = apps.get_model("core", "ObjectType")
41
+ ContentType = apps.get_model("contenttypes", "ContentType")
40
42
 
41
43
  defaults = {
42
44
  "label": label,
@@ -47,23 +49,29 @@ def create_custom_field(
47
49
  "ui_editable": getattr(CustomFieldUIEditableChoices, "NO"),
48
50
  }
49
51
 
50
- custom_field, _ = apps.get_model("extras", "CustomField").objects.update_or_create(
51
- type=getattr(CustomFieldTypeChoices, cf_type.upper()),
52
- name=field_name,
53
- defaults=defaults,
52
+ custom_field, _ = (
53
+ apps.get_model("extras", "CustomField")
54
+ .objects.using(db_alias)
55
+ .update_or_create(
56
+ type=getattr(CustomFieldTypeChoices, cf_type.upper()),
57
+ name=field_name,
58
+ defaults=defaults,
59
+ )
54
60
  )
55
61
 
56
62
  for model in target_models:
57
- custom_field.object_types.add(ObjectType.objects.get_for_model(model))
63
+ custom_field.object_types.add(ContentType.objects.get_for_model(model))
58
64
 
59
65
 
60
66
  def prepare_custom_fields(apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"):
61
67
  """Forward migration to prepare ipfabric_netbox custom fields and links."""
68
+ db_alias = schema_editor.connection.alias
62
69
  Device = apps.get_model("dcim", "Device")
63
70
  Site = apps.get_model("dcim", "Site")
64
71
 
65
72
  create_custom_field(
66
73
  apps,
74
+ db_alias,
67
75
  "ipfabric_source",
68
76
  "IP Fabric Source",
69
77
  [Device, Site],
@@ -72,31 +80,41 @@ def prepare_custom_fields(apps: "apps_type", schema_editor: "BaseDatabaseSchemaE
72
80
  )
73
81
  create_custom_field(
74
82
  apps,
83
+ db_alias,
75
84
  "ipfabric_ingestion",
76
85
  "IP Fabric Last Ingestion",
77
86
  [Device, Site],
78
87
  cf_type="type_object",
79
88
  object_type=apps.get_model("ipfabric_netbox", "IPFabricIngestion"),
80
89
  )
81
- cl, _ = apps.get_model("extras", "CustomLink").objects.update_or_create(
82
- defaults={
83
- "link_text": "{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}IP Fabric{% endif %}{% endif %}",
84
- "link_url": '{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}{% set BASE_URL = object.custom_fields.filter(related_object_type__model="ipfabricsource").first().related_object_type.model_class().objects.get(pk=SOURCE_ID).url %}{{ BASE_URL }}/inventory/devices?options={"filters":{"sn": ["like","{{ object.serial }}"]}}{% endif %}{%endif%}',
85
- "new_window": True,
86
- "button_class": CustomLinkButtonClassChoices.BLUE,
87
- },
88
- name="ipfabric",
90
+ cl, _ = (
91
+ apps.get_model("extras", "CustomLink")
92
+ .objects.using(db_alias)
93
+ .update_or_create(
94
+ defaults={
95
+ "link_text": "{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}IP Fabric{% endif %}{% endif %}",
96
+ "link_url": '{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}{% set BASE_URL = object.custom_fields.filter(related_object_type__model="ipfabricsource").first().related_object_type.model_class().objects.get(pk=SOURCE_ID).url %}{{ BASE_URL }}/inventory/devices?options={"filters":{"sn": ["like","{{ object.serial }}"]}}{% endif %}{%endif%}',
97
+ "new_window": True,
98
+ "button_class": CustomLinkButtonClassChoices.BLUE,
99
+ },
100
+ name="ipfabric",
101
+ )
89
102
  )
90
103
  cl.object_types.add(
91
- apps.get_model("core", "ObjectType").objects.get_for_model(Device)
104
+ apps.get_model("contenttypes", "ContentType")
105
+ .objects.db_manager(db_alias)
106
+ .get_for_model(Device)
92
107
  )
93
108
 
94
109
 
95
110
  def cleanup_custom_fields(apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"):
96
111
  """Reverse migration to prepare ipfabric_netbox custom fields and links."""
112
+ db_alias = schema_editor.connection.alias
97
113
  for field_name in ["ipfabric_source", "ipfabric_ingestion"]:
98
- custom_field = apps.get_model("extras", "CustomField").objects.get(
99
- name=field_name
114
+ custom_field = (
115
+ apps.get_model("extras", "CustomField")
116
+ .objects.using(db_alias)
117
+ .get(name=field_name)
100
118
  )
101
119
  for model in custom_field.object_types.all()[:]:
102
120
  custom_field.object_types.remove(model)
@@ -109,7 +127,9 @@ def prepare_transform_maps(
109
127
  """Create transform maps if they do not exist yet.
110
128
  They used to be created during plugin.ready() so they might be present on older DBs.
111
129
  """
112
- build_transform_maps(data=get_transform_map(), apps=apps)
130
+ build_transform_maps(
131
+ data=get_transform_map(), apps=apps, db_alias=schema_editor.connection.alias
132
+ )
113
133
 
114
134
 
115
135
  class Migration(migrations.Migration):
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.2.5 on 2025-09-22 07:43
2
+ from django.db import migrations
3
+
4
+
5
+ class Migration(migrations.Migration):
6
+ dependencies = [
7
+ ("ipfabric_netbox", "0018_remove_type_field"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AlterModelOptions(
12
+ name="ipfabrictransformmap",
13
+ options={"ordering": ("pk",)},
14
+ ),
15
+ migrations.AlterModelOptions(
16
+ name="ipfabrictransformmapgroup",
17
+ options={"ordering": ("pk",)},
18
+ ),
19
+ ]
ipfabric_netbox/models.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import ast
2
+ import functools
2
3
  import json
3
4
  import logging
4
5
  import traceback
@@ -35,10 +36,10 @@ from netbox.models import NetBoxModel
35
36
  from netbox.models import PrimaryModel
36
37
  from netbox.models.features import JobsMixin
37
38
  from netbox.models.features import TagsMixin
38
- from netbox.registry import registry
39
39
  from netbox_branching.choices import BranchStatusChoices
40
40
  from netbox_branching.contextvars import active_branch
41
41
  from netbox_branching.models import Branch
42
+ from netbox_branching.utilities import supports_branching
42
43
  from utilities.querysets import RestrictedQuerySet
43
44
  from utilities.request import NetBoxFakeRequest
44
45
 
@@ -176,13 +177,17 @@ class IPFabricTransformMap(NetBoxModel):
176
177
  )
177
178
  return cleaned_data
178
179
 
180
+ @functools.cache
179
181
  def get_models(self):
180
182
  _context = dict()
181
183
 
182
- for app, model_names in registry["model_features"]["custom_fields"].items():
184
+ for app, app_models in apps.all_models.items():
183
185
  _context.setdefault(app, {})
184
- for model_name in model_names:
185
- model = apps.get_registered_model(app, model_name)
186
+ for model in app_models:
187
+ if isinstance(model, str):
188
+ model = apps.get_registered_model(app, model)
189
+ if not supports_branching(model):
190
+ continue
186
191
  _context[app][model.__name__] = model
187
192
  _context["contenttypes"] = {}
188
193
  _context["contenttypes"]["ContentType"] = ContentType
@@ -249,10 +254,16 @@ class IPFabricTransformMap(NetBoxModel):
249
254
  # See NetBox docs Customization -> Custom Scripts -> Change Logging
250
255
  instance = queryset.get(**context)
251
256
  instance.snapshot()
257
+ changed = False
252
258
  for attr, value in defaults.items():
259
+ # Only run data validation and save if something has changed
260
+ if getattr(instance, attr) == value:
261
+ continue
262
+ changed = True
253
263
  setattr(instance, attr, value)
254
- instance.full_clean()
255
- instance.save(using=connection_name)
264
+ if changed:
265
+ instance.full_clean()
266
+ instance.save(using=connection_name)
256
267
  except target_class.DoesNotExist:
257
268
  for field in list(context.keys()):
258
269
  # When assigning we need to replace `field__isnull=True` with `field=None`
@@ -9,7 +9,7 @@ logger = logging.getLogger("ipfabric_netbox.template_content")
9
9
 
10
10
 
11
11
  class SiteTopologyButtons(PluginTemplateExtension):
12
- model = "dcim.site"
12
+ models = ["dcim.site"]
13
13
 
14
14
  def buttons(self):
15
15
  try:
@@ -1,4 +1,3 @@
1
- {% if object.custom_field_data.ipfabric_source %}
2
1
  <div class="btn-group" role="group" aria-label="Button group with nested dropdown">
3
2
  <div class="btn-group" role="group">
4
3
  <button id="btnGroupDrop1" type="button" class="btn btn-sm btn-success dropdown-toggle" data-bs-toggle="dropdown"
@@ -48,7 +47,6 @@
48
47
  </div>
49
48
  </div>
50
49
  </div>
51
- {% endif %}
52
50
 
53
51
  {% block javascript %}
54
52
  <script>
@@ -3,6 +3,7 @@
3
3
  {% load helpers %}
4
4
  {% load plugins %}
5
5
  {% load render_table from django_tables2 %}
6
+ {% load perms %}
6
7
  {% load ipfabric_netbox_helpers %}
7
8
 
8
9
  {% block extra_controls %}
@@ -22,7 +23,7 @@
22
23
  </span>
23
24
  {% endif %}
24
25
  {% endif %}
25
- {% endblock %}
26
+ {% endblock extra_controls %}
26
27
 
27
28
  {% block content %}
28
29
  <div class="row mb-3">
@@ -620,7 +620,7 @@ class IPFabricSyncTest(APIViewTestCases.APIViewTestCase):
620
620
  name="Sync Test E",
621
621
  snapshot_data=snapshots[1],
622
622
  parameters={"device": True, "interface": True},
623
- auto_merge=True,
623
+ auto_merge=False,
624
624
  )
625
625
  IPFabricSync.objects.create(
626
626
  name="Sync Test F",
@@ -808,13 +808,6 @@ class IPFabricSyncFormTestCase(TestCase):
808
808
  )
809
809
  self.assertTrue(form.is_valid(), form.errors)
810
810
 
811
- def test_form_initialization_with_source_no_data(self):
812
- """Test source handling without data"""
813
- form = IPFabricSyncForm(initial={"source": self.source.pk})
814
-
815
- # Verify that source_type is set when there's a source and no data
816
- self.assertEqual(form.source_type, IPFabricSourceTypeChoices.LOCAL)
817
-
818
811
  def test_form_initialization_with_sites_no_data(self):
819
812
  """Test sites handling without data"""
820
813
  form = IPFabricSyncForm(initial={"sites": ["site1", "site2"]})
@@ -905,9 +898,6 @@ class IPFabricSyncFormTestCase(TestCase):
905
898
  # Test form initialization with existing instance but no data
906
899
  form = IPFabricSyncForm(instance=sync_instance)
907
900
 
908
- # Verify that source_type is set from the instance
909
- self.assertEqual(form.source_type, IPFabricSourceTypeChoices.LOCAL)
910
-
911
901
  # Verify that initial values are set from instance parameters
912
902
  self.assertEqual(form.initial["source"], self.source)
913
903
  self.assertEqual(form.initial["sites"], ["site1", "site2"])
@@ -932,18 +922,121 @@ class IPFabricSyncFormTestCase(TestCase):
932
922
  instance=sync_instance, initial={"name": "Override Name"}
933
923
  )
934
924
 
935
- # When initial kwargs are provided, the form skips the instance initialization block
936
- # This means source_type remains None as intended by the form logic
937
- self.assertIsNone(form.source_type)
938
-
939
- # initial should not be set from instance when initial kwargs are provided
940
- self.assertNotIn("source", form.initial)
941
- self.assertNotIn("sites", form.initial)
942
- self.assertNotIn("groups", form.initial)
925
+ # These should be set from instance even when not in initial kwarg
926
+ self.assertIn("source", form.initial)
927
+ self.assertIn("sites", form.initial)
928
+ self.assertIn("groups", form.initial)
943
929
 
944
930
  # But the provided initial value should be present
945
931
  self.assertEqual(form.initial.get("name"), "Override Name")
946
932
 
933
+ def test_sites_initial_value_set_from_form_initial(self):
934
+ """Test that sites field initial is set from self.initial["sites"]"""
935
+ # Create an existing sync instance with sites in parameters
936
+ sync_instance = IPFabricSync.objects.create(
937
+ name="Existing Sync",
938
+ snapshot_data=self.snapshot,
939
+ parameters={
940
+ "sites": ["site1", "site2"],
941
+ "groups": [self.transform_map_group.pk],
942
+ },
943
+ )
944
+
945
+ # Initialize form with existing instance and additional initial data for sites
946
+ # This will trigger the else branch where self.initial["sites"] is used
947
+ form = IPFabricSyncForm(
948
+ instance=sync_instance,
949
+ initial={"sites": ["override_site1", "override_site2"]},
950
+ )
951
+
952
+ # Verify sites field initial is set from self.initial
953
+ self.assertEqual(
954
+ form.fields["sites"].initial, ["override_site1", "override_site2"]
955
+ )
956
+
957
+ # Also verify that self.initial contains the expected sites
958
+ self.assertEqual(form.initial["sites"], ["override_site1", "override_site2"])
959
+
960
+ def test_htmx_boolean_field_list_values_handled(self):
961
+ """Test sanitizing HTMX BooleanField list values like ['', 'on']"""
962
+ # Simulate HTMX request where BooleanField values become lists
963
+ # This happens when `source` field value is changed and form is re-drawn via HTMX
964
+ form = IPFabricSyncForm(
965
+ initial={
966
+ "auto_merge": ["", "on"], # HTMX sends BooleanField as list
967
+ "update_custom_fields": ["", "on"], # Another BooleanField as list
968
+ "ipf_site": ["", "on"], # ipf_ prefixed field as list
969
+ "name": "Test Sync", # Normal field (not affected)
970
+ },
971
+ data={
972
+ "name": "Test Sync HTMX",
973
+ "source": self.source.pk,
974
+ "snapshot_data": self.snapshot.pk,
975
+ },
976
+ )
977
+
978
+ # The last value from ['', 'on'] should be 'on' which evaluates to True for BooleanFields
979
+ self.assertEqual(form.initial["auto_merge"], "on")
980
+ self.assertEqual(form.initial["update_custom_fields"], "on")
981
+ self.assertEqual(form.initial["ipf_site"], "on")
982
+ self.assertEqual(form.initial["name"], "Test Sync") # Normal field unchanged
983
+
984
+ # Verify the form is still valid and processes correctly
985
+ self.assertTrue(form.is_valid(), form.errors)
986
+
987
+ def test_htmx_boolean_field_single_values_unchanged(self):
988
+ """Test that normal single values are not affected by the HTMX list handling"""
989
+ # Test with normal single values (not lists)
990
+ form = IPFabricSyncForm(
991
+ initial={
992
+ "auto_merge": True, # Normal boolean value
993
+ "update_custom_fields": False, # Normal boolean value
994
+ "ipf_site": "on", # Normal string value
995
+ "name": "Test Sync", # Normal string value
996
+ },
997
+ data={
998
+ "name": "Test Sync Normal",
999
+ "source": self.source.pk,
1000
+ "snapshot_data": self.snapshot.pk,
1001
+ },
1002
+ )
1003
+
1004
+ # Verify that single values are not processed by value sanitization
1005
+ self.assertEqual(form.initial["auto_merge"], True)
1006
+ self.assertEqual(form.initial["update_custom_fields"], False)
1007
+ self.assertEqual(form.initial["ipf_site"], "on")
1008
+ self.assertEqual(form.initial["name"], "Test Sync")
1009
+
1010
+ # Verify the form is still valid
1011
+ self.assertTrue(form.is_valid(), form.errors)
1012
+
1013
+ def test_clean_snapshot_does_not_belong_to_source(self):
1014
+ """Test form validation when snapshot doesn't belong to the selected source"""
1015
+ # Create a second source
1016
+ different_source = IPFabricSource.objects.create(
1017
+ name="Different Source",
1018
+ type=IPFabricSourceTypeChoices.LOCAL,
1019
+ url="https://different.ipfabric.local",
1020
+ status=DataSourceStatusChoices.NEW,
1021
+ )
1022
+
1023
+ # Try to use self.snapshot (which belongs to self.source) with different_source
1024
+ form = IPFabricSyncForm(
1025
+ data={
1026
+ "name": "Test Sync Mismatched Source",
1027
+ "source": different_source.pk,
1028
+ "snapshot_data": self.snapshot.pk, # This snapshot belongs to self.source, not different_source
1029
+ }
1030
+ )
1031
+
1032
+ # Form should be invalid due to snapshot/source mismatch validation
1033
+ self.assertFalse(form.is_valid(), form.errors)
1034
+ self.assertIn("snapshot_data", form.errors)
1035
+ self.assertTrue(
1036
+ "Snapshot does not belong to the selected source"
1037
+ in str(form.errors["snapshot_data"])
1038
+ )
1039
+
947
1040
  def test_clean_sites_not_part_of_snapshot(self):
948
1041
  """Test form validation when selected sites are not part of the snapshot"""
949
1042
  form = IPFabricSyncForm(
@@ -37,6 +37,7 @@ from ipfabric_netbox.models import IPFabricSync
37
37
  from ipfabric_netbox.models import IPFabricTransformField
38
38
  from ipfabric_netbox.models import IPFabricTransformMap
39
39
  from ipfabric_netbox.models import IPFabricTransformMapGroup
40
+ from ipfabric_netbox.tables import DeviceIPFTable
40
41
 
41
42
 
42
43
  class PluginPathMixin:
@@ -1971,7 +1972,19 @@ class IPFabricTableViewTestCase(PluginPathMixin, ModelTestCase):
1971
1972
  # Validate context variables
1972
1973
  self.assertEqual(response.context["object"], self.device)
1973
1974
  self.assertIsNotNone(response.context["form"])
1974
- self.assertEqual(response.context["table"], tableChoices[0][0])
1975
+
1976
+ table = response.context["table"]
1977
+ self.assertIsInstance(table, DeviceIPFTable)
1978
+
1979
+ # Verify the table has the expected structure for empty data scenario
1980
+ self.assertEqual(len(table.data), 0) # Should be empty when no source
1981
+ self.assertIn(
1982
+ "hostname", [col.name for col in table.columns]
1983
+ ) # Should have default hostname column
1984
+
1985
+ # Verify table meta attributes
1986
+ self.assertEqual(table.empty_text, "No results found")
1987
+ self.assertIn("table-hover", table.attrs.get("class", ""))
1975
1988
 
1976
1989
  # When no source is available, source should be None
1977
1990
  self.assertIsNone(response.context["source"])
@@ -8,10 +8,10 @@ from django.apps import apps as django_apps
8
8
  # see https://docs.djangoproject.com/en/5.1/topics/migrations/#historical-models
9
9
 
10
10
 
11
- def build_fields(data, apps):
11
+ def build_fields(data, apps, db_alias):
12
12
  ContentType = apps.get_model("contenttypes", "ContentType")
13
13
  if "target_model" in data:
14
- ct = ContentType.objects.get_for_model(
14
+ ct = ContentType.objects.db_manager(db_alias).get_for_model(
15
15
  apps.get_model(
16
16
  data["target_model"]["app_label"],
17
17
  data["target_model"]["model"],
@@ -19,7 +19,7 @@ def build_fields(data, apps):
19
19
  )
20
20
  data["target_model"] = ct
21
21
  elif "source_model" in data:
22
- ct = ContentType.objects.get_for_model(
22
+ ct = ContentType.objects.db_manager(db_alias).get_for_model(
23
23
  apps.get_model(
24
24
  data["source_model"]["app_label"],
25
25
  data["source_model"]["model"],
@@ -29,7 +29,7 @@ def build_fields(data, apps):
29
29
  return data
30
30
 
31
31
 
32
- def build_transform_maps(data, apps: django_apps = None):
32
+ def build_transform_maps(data, apps: django_apps = None, db_alias: str = "default"):
33
33
  apps = apps or django_apps
34
34
  IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
35
35
  IPFabricTransformField = apps.get_model("ipfabric_netbox", "IPFabricTransformField")
@@ -37,14 +37,16 @@ def build_transform_maps(data, apps: django_apps = None):
37
37
  "ipfabric_netbox", "IPFabricRelationshipField"
38
38
  )
39
39
  for tm in data:
40
- field_data = build_fields(tm["data"], apps)
41
- tm_obj = IPFabricTransformMap.objects.create(**field_data)
40
+ field_data = build_fields(tm["data"], apps, db_alias)
41
+ tm_obj = IPFabricTransformMap.objects.using(db_alias).create(**field_data)
42
42
  for fm in tm["field_maps"]:
43
- field_data = build_fields(fm, apps)
44
- IPFabricTransformField.objects.create(transform_map=tm_obj, **field_data)
43
+ field_data = build_fields(fm, apps, db_alias)
44
+ IPFabricTransformField.objects.using(db_alias).create(
45
+ transform_map=tm_obj, **field_data
46
+ )
45
47
  for rm in tm["relationship_maps"]:
46
- relationship_data = build_fields(rm, apps)
47
- IPFabricRelationshipField.objects.create(
48
+ relationship_data = build_fields(rm, apps, db_alias)
49
+ IPFabricRelationshipField.objects.using(db_alias).create(
48
50
  transform_map=tm_obj, **relationship_data
49
51
  )
50
52
 
ipfabric_netbox/views.py CHANGED
@@ -544,13 +544,13 @@ class IPFabricSyncEditView(generic.ObjectEditView):
544
544
  @register_model_view(IPFabricSync)
545
545
  class IPFabricSyncView(generic.ObjectView):
546
546
  queryset = IPFabricSync.objects.all()
547
- actions = ("edit",)
548
547
 
549
548
  def get(self, request, **kwargs):
550
- instance = self.get_object(**kwargs)
551
- last_ingestion = instance.ipfabricingestion_set.last()
552
-
549
+ # Handle HTMX requests separately
553
550
  if request.htmx:
551
+ instance = self.get_object(**kwargs)
552
+ last_ingestion = instance.ipfabricingestion_set.last()
553
+
554
554
  response = render(
555
555
  request,
556
556
  "ipfabric_netbox/partials/sync_last_ingestion.html",
@@ -565,15 +565,8 @@ class IPFabricSyncView(generic.ObjectView):
565
565
  response["HX-Refresh"] = "true"
566
566
  return response
567
567
 
568
- return render(
569
- request,
570
- self.get_template_name(),
571
- {
572
- "object": instance,
573
- "tab": self.tab,
574
- **self.get_extra_context(request, instance),
575
- },
576
- )
568
+ # For regular requests, use the parent method which includes actions
569
+ return super().get(request, **kwargs)
577
570
 
578
571
  def get_extra_context(self, request, instance):
579
572
  if request.GET.get("format") in ["json", "yaml"]:
@@ -641,6 +634,26 @@ class IPFabricTransformMapTabView(generic.ObjectChildrenView):
641
634
  )
642
635
 
643
636
 
637
+ @register_model_view(IPFabricSync, "ingestion")
638
+ class IPFabricIngestionTabView(generic.ObjectChildrenView):
639
+ queryset = IPFabricSync.objects.all()
640
+ child_model = IPFabricIngestion
641
+ table = IPFabricIngestionTable
642
+ filterset = IPFabricIngestionFilterSet
643
+ tab = ViewTab(
644
+ label="Ingestions",
645
+ badge=lambda obj: IPFabricIngestion.objects.filter(sync=obj).count(),
646
+ permission="ipfabric_netbox.view_ipfabricingestion",
647
+ )
648
+
649
+ def get_children(self, request, parent):
650
+ return self.child_model.objects.filter(sync=parent).annotate(
651
+ description=models.F("branch__description"),
652
+ user=models.F("sync__user__username"),
653
+ staged_changes=models.Count(models.F("branch__changediff")),
654
+ )
655
+
656
+
644
657
  # Ingestion
645
658
  class IPFabricIngestionListView(generic.ObjectListView):
646
659
  queryset = IPFabricIngestion.objects.annotate(
@@ -870,34 +883,15 @@ class IPFabricIngestionDeleteView(generic.ObjectDeleteView):
870
883
  queryset = IPFabricIngestion.objects.all()
871
884
 
872
885
 
873
- @register_model_view(IPFabricSync, "ingestion")
874
- class IPFabricIngestionTabView(generic.ObjectChildrenView):
875
- queryset = IPFabricSync.objects.all()
876
- child_model = IPFabricIngestion
877
- table = IPFabricIngestionTable
878
- filterset = IPFabricIngestionFilterSet
879
- template_name = "generic/object_children.html"
880
- tab = ViewTab(
881
- label="Ingestions",
882
- badge=lambda obj: IPFabricIngestion.objects.filter(sync=obj).count(),
883
- permission="ipfabric_netbox.view_ipfabricingestion",
884
- )
885
-
886
- def get_children(self, request, parent):
887
- return self.child_model.objects.filter(sync=parent).annotate(
888
- description=models.F("branch__description"),
889
- user=models.F("sync__user__username"),
890
- staged_changes=models.Count(models.F("branch__changediff")),
891
- )
892
-
893
-
894
886
  @register_model_view(Device, "ipfabric")
895
- class IPFabricTable(View):
887
+ class IPFabricTable(generic.ObjectView):
896
888
  template_name = "ipfabric_netbox/ipfabric_table.html"
897
889
  tab = ViewTab("IP Fabric", permission="ipfabric_netbox.view_devicetable")
890
+ queryset = Device.objects.all()
898
891
 
899
- def get(self, request, pk):
900
- device = get_object_or_404(Device, pk=pk)
892
+ def get_extra_context(self, request, instance):
893
+ """Process form and prepare table data for the template."""
894
+ device = instance
901
895
  form = (
902
896
  IPFabricTableForm(request.GET)
903
897
  if "table" in request.GET
@@ -908,7 +902,7 @@ class IPFabricTable(View):
908
902
  source = None
909
903
 
910
904
  if form.is_valid():
911
- table = form.cleaned_data["table"]
905
+ table_name = form.cleaned_data["table"]
912
906
  test = {
913
907
  "True": True,
914
908
  "False": False,
@@ -929,36 +923,24 @@ class IPFabricTable(View):
929
923
  snapshot_id = form.cleaned_data["snapshot_data"].snapshot_id
930
924
  source = source or form.cleaned_data["snapshot_data"].source
931
925
 
932
- if source is None:
933
- return render(
934
- request,
935
- self.template_name,
936
- {
937
- "object": device,
938
- "source": source,
939
- "tab": self.tab,
940
- "form": form,
941
- "table": table,
942
- },
943
- )
944
-
945
- source.parameters["snapshot_id"] = snapshot_id
946
- source.parameters["base_url"] = source.url
947
-
948
- cache_key = (
949
- f"ipfabric_{table}_{device.serial}_{source.parameters['snapshot_id']}"
950
- )
951
- if cache_enable:
952
- data = cache.get(cache_key)
953
-
954
- if not data:
955
- try:
956
- ipf = IPFabric(parameters=source.parameters)
957
- raw_data, columns = ipf.get_table_data(table=table, device=device)
958
- data = {"data": raw_data, "columns": columns}
959
- cache.set(cache_key, data, 60 * 60 * 24)
960
- except Exception as e:
961
- messages.error(request, e)
926
+ if source is not None:
927
+ source.parameters["snapshot_id"] = snapshot_id
928
+ source.parameters["base_url"] = source.url
929
+
930
+ cache_key = f"ipfabric_{table_name}_{device.serial}_{source.parameters['snapshot_id']}"
931
+ if cache_enable:
932
+ data = cache.get(cache_key)
933
+
934
+ if not data:
935
+ try:
936
+ ipf = IPFabric(parameters=source.parameters)
937
+ raw_data, columns = ipf.get_table_data(
938
+ table=table_name, device=device
939
+ )
940
+ data = {"data": raw_data, "columns": columns}
941
+ cache.set(cache_key, data, 60 * 60 * 24)
942
+ except Exception as e:
943
+ messages.error(request, e)
962
944
 
963
945
  if not data:
964
946
  data = {"data": [], "columns": []}
@@ -973,32 +955,34 @@ class IPFabricTable(View):
973
955
  },
974
956
  ).configure(table)
975
957
 
958
+ if not source:
959
+ if source_id := device.custom_field_data.get("ipfabric_source"):
960
+ source = IPFabricSource.objects.filter(pk=source_id).first()
961
+ else:
962
+ source = IPFabricSource.get_for_site(device.site).first()
963
+
964
+ return {
965
+ "source": source,
966
+ "form": form,
967
+ "table": table,
968
+ }
969
+
970
+ def get(self, request, **kwargs):
971
+ """Handle GET requests, with special handling for HTMX table updates."""
972
+ # For HTMX requests, we only need to return the table HTML
976
973
  if request.htmx:
974
+ device = get_object_or_404(Device, pk=kwargs.get("pk"))
975
+ context = self.get_extra_context(request, device)
977
976
  return render(
978
977
  request,
979
978
  "htmx/table.html",
980
979
  {
981
- "table": table,
980
+ "table": context["table"],
982
981
  },
983
982
  )
984
983
 
985
- if not source:
986
- if source_id := device.custom_field_data.get("ipfabric_source"):
987
- source = IPFabricSource.objects.filter(pk=source_id).first()
988
- else:
989
- source = IPFabricSource.get_for_site(device.site).first()
990
-
991
- return render(
992
- request,
993
- self.template_name,
994
- {
995
- "object": device,
996
- "source": source,
997
- "tab": self.tab,
998
- "form": form,
999
- "table": table,
1000
- },
1001
- )
984
+ # For regular requests, use the parent's get() method which will call get_extra_context()
985
+ return super().get(request, **kwargs)
1002
986
 
1003
987
 
1004
988
  @register_model_view(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipfabric_netbox
3
- Version: 4.2.2b3
3
+ Version: 4.3.0
4
4
  Summary: NetBox plugin to sync IP Fabric data into NetBox
5
5
  License: MIT
6
6
  Keywords: netbox,ipfabric,plugin,sync
@@ -24,7 +24,7 @@ Requires-Dist: ipfabric (>=6.6.4) ; extra != "ipfabric_6_10" and extra != "ipfab
24
24
  Requires-Dist: ipfabric (>=7.0.0,<7.1.0) ; extra != "ipfabric_6_10" and extra == "ipfabric_7_0" and extra != "ipfabric_7_2" and extra != "ipfabric_7_3"
25
25
  Requires-Dist: ipfabric (>=7.2.0,<7.3.0) ; extra != "ipfabric_6_10" and extra != "ipfabric_7_0" and extra == "ipfabric_7_2" and extra != "ipfabric_7_3"
26
26
  Requires-Dist: ipfabric (>=7.3.0,<7.4.0) ; extra != "ipfabric_6_10" and extra != "ipfabric_7_0" and extra != "ipfabric_7_2" and extra == "ipfabric_7_3"
27
- Requires-Dist: netboxlabs-netbox-branching (>=0.5.5,<0.6.0)
27
+ Requires-Dist: netboxlabs-netbox-branching (==0.7.0)
28
28
  Requires-Dist: netutils
29
29
  Project-URL: Bug Tracker, https://gitlab.com/ip-fabric/integrations/ipfabric-netbox-sync/-/issues
30
30
  Project-URL: Homepage, https://gitlab.com/ip-fabric/integrations/ipfabric-netbox-sync
@@ -65,7 +65,9 @@ These are the required NetBox versions for corresponding plugin version. Any oth
65
65
 
66
66
  | Netbox Version | Plugin Version |
67
67
  |----------------|----------------|
68
- | 4.3.0 and up | 4.0.0 and up |
68
+ | 4.4.0 and up | 4.3.0 and up |
69
+ | 4.3.0 - 4.3.7 | 4.2.2 |
70
+ | 4.3.0 - 4.3.6 | 4.0.0 - 4.2.1 |
69
71
  | 4.2.4 - 4.2.9 | 3.2.2 - 3.2.4 |
70
72
  | 4.2.0 - 4.2.3 | 3.2.0 |
71
73
  | 4.1.5 - 4.1.11 | 3.1.1 - 3.1.3 |
@@ -1,4 +1,4 @@
1
- ipfabric_netbox/__init__.py,sha256=AZVb97A93_y_k7vzGEVgKEnqe6xe1sYTdaWvIoKLPD0,674
1
+ ipfabric_netbox/__init__.py,sha256=wKd54GhT35mTJhN4456jwJ0_KKNQU5JFH3BsKfqDtLs,672
2
2
  ipfabric_netbox/api/__init__.py,sha256=XRclTGWVR0ZhAAwgYul5Wm_loug5_hUjEumbLQEwKYM,47
3
3
  ipfabric_netbox/api/serializers.py,sha256=7cmVsIzGzz9u6htLKizLr2Ar0OC7uV8rMX3U7EzRmG4,6482
4
4
  ipfabric_netbox/api/urls.py,sha256=1fXXVTxNY5E64Nfz6b7zXD9bZI3FcefuxAWKMe0w_QU,1240
@@ -7,7 +7,7 @@ ipfabric_netbox/choices.py,sha256=27ePh1IUU3zETbceNcAgjsHvqhPlGpayFRFr5luqW1k,50
7
7
  ipfabric_netbox/data/transform_map.json,sha256=4PsucgMHcLW3SPoKEptQCd0gA5tCF4hjrR4bGQFCWy8,21744
8
8
  ipfabric_netbox/exceptions.py,sha256=DT4dpbakvqoROtBR_F0LzvQCMNWpGhufFcUbZTx0OLY,2655
9
9
  ipfabric_netbox/filtersets.py,sha256=vaWlxf8DTwduv_aQ35kJxwyzmM1XvE781GjUj2z4QGQ,7845
10
- ipfabric_netbox/forms.py,sha256=T1iqsHLBXN315TTMGB0XU_iNIi3eju4DzsaWMjftt0M,43205
10
+ ipfabric_netbox/forms.py,sha256=2qs4tTq51mRilb9qSoB634TFKhFrOYGeFBKLf8kD9vU,45913
11
11
  ipfabric_netbox/graphql/__init__.py,sha256=-a5w_VY7pc-RVt8MvThkTzeAqCC3xCan4Ue6iMefmjI,754
12
12
  ipfabric_netbox/graphql/enums.py,sha256=QFhwiwUKJekxQfsOGk_-70_WnkzrKEP_zIBMrin0S0Q,1343
13
13
  ipfabric_netbox/graphql/filters.py,sha256=B8xy9r9a18vWfV6a6tHXAN1FUcoxI6MOrbsdNmzusNI,12991
@@ -15,7 +15,7 @@ ipfabric_netbox/graphql/schema.py,sha256=5UVHA1hHRvho5eLuuS-HLXTVTbxpUUx68ovG03g
15
15
  ipfabric_netbox/graphql/types.py,sha256=8RxdxiA-WnoaWSzh-tUJCuZBYGmd6QjfJiJcLirRMKY,5961
16
16
  ipfabric_netbox/jobs.py,sha256=KrTUeCuFUIU7vKCUS3RiBYCBG7g7GzhGagM_qFMGQJ4,3089
17
17
  ipfabric_netbox/migrations/0001_initial.py,sha256=VphxkWL6QzWq2tcrdXlog718xQtiEGizKwS830z_fOs,13824
18
- ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py,sha256=xDj5QJfTmG7ZgUSTRiJRndjSOv25dORtzzs-LySbr-c,20874
18
+ ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py,sha256=OvofuA8ImeJmjrbtCrZPcRxAUWx2Ww4DUXLBZYsy6qE,21381
19
19
  ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py,sha256=xQpouHjOutyj6riN2B592njzSvz_icpkUbo5W7nWLYw,431
20
20
  ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py,sha256=XUM_ZecLlzElKYfBg9-cIB-cUy1YHpIzvJhvWP-dbIw,1579
21
21
  ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py,sha256=hlwHGKD7q3w1a1N04yIS39f6nsL_9TJCWEJlvwCdpPA,435
@@ -33,18 +33,19 @@ ipfabric_netbox/migrations/0015_ipfabricingestionissue.py,sha256=AjAkyboa4BSXsN5
33
33
  ipfabric_netbox/migrations/0016_tags_and_changelog_for_snapshots.py,sha256=XqftTQ4GFnoCoGSHPa2WL_bjSVCGxdP2MFXCUa6LN1k,929
34
34
  ipfabric_netbox/migrations/0017_ipfabricsync_update_custom_fields.py,sha256=IVbAL2WdigYT40sXN0A8K3HweJ_O4QqyzjB06TbkG5E,447
35
35
  ipfabric_netbox/migrations/0018_remove_type_field.py,sha256=ffxW6IS3BLCbvM5M9DbDb_x6spMmRxnV1iq8IuXxMGw,385
36
+ ipfabric_netbox/migrations/0019_alter_ipfabrictransformmap_options_and_more.py,sha256=ieDVedt9KpJBicAiC3kdZXzHeos12N0L9EdRXKmIVgY,501
36
37
  ipfabric_netbox/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- ipfabric_netbox/models.py,sha256=o3iOsgXUY5bAsx8NGeRtA0W1ZPkBiz5n5Q3spu71nQo,36504
38
+ ipfabric_netbox/models.py,sha256=y2S3uhUSLR_i2ysE1LxJXonbUTAU23ftO2oxdjIgc1Y,36923
38
39
  ipfabric_netbox/navigation.py,sha256=2dEJ_wKHb52Tl0FOV1TH3JbxRe8YZ56ewrTsBFGKpCg,2210
39
40
  ipfabric_netbox/signals.py,sha256=cGa5PVD2i24pGXiVNfbu6ruIDqPVdwKQHTSWe9Ura84,1838
40
41
  ipfabric_netbox/tables.py,sha256=zJUCoOrJgBNVxOmQrvlUyoD_0_Kq988GjregdcSAa68,8854
41
- ipfabric_netbox/template_content.py,sha256=lucsGn5zzEjcwFUx1yvL458HCRPB8CpAH4dJD3R0TJo,1117
42
+ ipfabric_netbox/template_content.py,sha256=lxZ02BFVihbSgjXCETGsWmhdElQUUO3uUGd0WfhlRmw,1120
42
43
  ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html,sha256=K-2TTDaS1F4wUIR8FFFPqex4KJbySXtHiz5V-OEwelY,967
43
44
  ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html,sha256=xOiIrvRIBtqDD65u6xcLo2xdwDKNpAylDmzznaJRGCw,3281
44
45
  ipfabric_netbox/templates/ipfabric_netbox/inc/json.html,sha256=qPHUdaHyKM9Y7FqBEnYwcNoyp5GMFaexJFXMRf4AqZQ,569
45
46
  ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html,sha256=JbS6wIE_zb0iBKo78W9Al8caXZY7HnsjEFDhY0hirw0,295
46
47
  ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html,sha256=HxGRk7oTptDWtoMklJ5TolbljZrvAzhZa_Hbjs75P8w,720
47
- ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html,sha256=0YOvokuo2ZbR48odRkCcEssWBIxwCGwpsWom38_l-FA,2806
48
+ ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html,sha256=iLd0W3rxAAVrgsYBIyro_88x5a2QFZZMc56EOAfOZsg,2744
48
49
  ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html,sha256=zjG5qjth277dnVkzyg8qxs72wtKZ8yypXRTXqral4Wk,2124
49
50
  ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html,sha256=1ItOCPjjp9iH7MLNLIz6x-2NnSa0Wx4bXUYVk-Jn4Y0,2885
50
51
  ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html,sha256=mRU-rBweVFvaPFHbVYPw7vcYyXiVaXCOkeHm7xWdKPA,500
@@ -53,7 +54,7 @@ ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html,sha256=TsF34lK2CyD
53
54
  ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html,sha256=fm_X2FLnoTS6s6AL3WmU6p3puDojROSkPG0jA4EBQeM,4435
54
55
  ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html,sha256=hj8ORs_4mM_xTjmw3McHN-da5seC8nbbkzobn0f1TSc,3482
55
56
  ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html,sha256=DQOA2TA7f1nI5YpxXthS1VzjIU1kMZus37l6bYSCauE,3869
56
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html,sha256=BicVS7mCP85fFZJEt46GUm5xppi1Jw3byw1el9BB2WE,4448
57
+ ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html,sha256=vgkxhJBWnfuZmDxfstLFJEAXc7FCY8Q7Hcu2MydI45A,4480
57
58
  ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html,sha256=qFo_Ku5oksx5co4HVtVq0xAVFI6CLWs-iBrwYzGsEGA,1460
58
59
  ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html,sha256=p8zqn0-B6mawSUM3zQrus6dsKUM5SRBTO0X94pLboX8,452
59
60
  ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html,sha256=TV7gAZWtSd-c7mzOen_nv7Z8MZr2Vw8vkHP4zW9au4w,2580
@@ -70,17 +71,17 @@ ipfabric_netbox/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
70
71
  ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py,sha256=STw4pAd2qG7hgf-O6UNTwsO5VqEa_gxf5wLv50BWL4Q,417
71
72
  ipfabric_netbox/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
73
  ipfabric_netbox/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
- ipfabric_netbox/tests/api/test_api.py,sha256=mZp0CUBJb0MUcnfKAWWrQ4Kab11OXtSI9XQtMZ4q40U,30282
74
- ipfabric_netbox/tests/test_forms.py,sha256=8sW0MHu2MtCDVHoaKM78U1_GioLjQk6UIvgN2Vijvv4,57518
74
+ ipfabric_netbox/tests/api/test_api.py,sha256=-pW0xRqHUEr574HFL27C3FE77slI_bEh3Y5nfPDfcao,30283
75
+ ipfabric_netbox/tests/test_forms.py,sha256=C8giV6E3PbMB9_864C12ebvfQ3Vlvdn39VIQQSP6GV8,61566
75
76
  ipfabric_netbox/tests/test_models.py,sha256=FFrIT5xxv_yvujKpxGjRJPNPBDF2Pqi8zbY0vxuJeQs,16043
76
- ipfabric_netbox/tests/test_views.py,sha256=znmNjDyJVDKfhhhaOheRb0nQBnncbvZNmA2DEQuKYj8,85259
77
+ ipfabric_netbox/tests/test_views.py,sha256=hki_95r5a8oc0D1jGKnrO2bKIwh3TWj6abFVXkgrTP0,85793
77
78
  ipfabric_netbox/urls.py,sha256=Bb5Znj24DrWJm4IZeI51wwe3KGA45TF4crNZaGBXmro,5467
78
79
  ipfabric_netbox/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
80
  ipfabric_netbox/utilities/ipfutils.py,sha256=wFmL5oriuF-is1ZlrIcLmoeYUY5ih-CA9weRQrx5AiA,31885
80
81
  ipfabric_netbox/utilities/logging.py,sha256=GYknjocMN6LQ2873_az3y0RKm29TCXaWviUIIneH-x0,3445
81
82
  ipfabric_netbox/utilities/nbutils.py,sha256=kFBEiJOGvr_49hJWCS2duXojx2-A9kVk0Xp_vj0ohfs,2641
82
- ipfabric_netbox/utilities/transform_map.py,sha256=QotbGc2TksINJrb62STgAigpC5Nsgi5umYHu_0rZd8k,2204
83
- ipfabric_netbox/views.py,sha256=vCfTvEG6gcaiLNLOAfDtezXArDA96jttfwmZLPvFWGs,38174
84
- ipfabric_netbox-4.2.2b3.dist-info/METADATA,sha256=08mjXZ5Hkoif2W2OoZQv7HnjBDilj-tGCxav0jas0zY,4689
85
- ipfabric_netbox-4.2.2b3.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
86
- ipfabric_netbox-4.2.2b3.dist-info/RECORD,,
83
+ ipfabric_netbox/utilities/transform_map.py,sha256=GpM_7Mm6FE0qV2qbyj4YfDn0l-JkeeEHQOZkNVSSHk4,2391
84
+ ipfabric_netbox/views.py,sha256=wzvRuUz4SKxFf3C9XMAZJmb9QquiavD5ofgwuci_ysw,38166
85
+ ipfabric_netbox-4.3.0.dist-info/METADATA,sha256=ak-B7W17Sbvm4Vis9lWXooQoEZdGszqYNP51Ae7a4U4,4752
86
+ ipfabric_netbox-4.3.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
87
+ ipfabric_netbox-4.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.0
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any