netbox-pathways 0.1.0__tar.gz → 0.2.0__tar.gz

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 (139) hide show
  1. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/PKG-INFO +49 -6
  2. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/README.md +47 -4
  3. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/__init__.py +8 -1
  4. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/api/serializers.py +31 -0
  5. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/choices.py +2 -0
  6. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/filterforms.py +27 -4
  7. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/filters.py +26 -7
  8. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/forms.py +164 -20
  9. netbox_pathways-0.2.0/netbox_pathways/graphql/filters.py +167 -0
  10. netbox_pathways-0.2.0/netbox_pathways/graphql/schema.py +67 -0
  11. netbox_pathways-0.2.0/netbox_pathways/graphql/types.py +116 -0
  12. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/management/commands/import_geodata.py +30 -2
  13. netbox_pathways-0.2.0/netbox_pathways/migrations/0015_installer_and_commissioned_date.py +39 -0
  14. netbox_pathways-0.2.0/netbox_pathways/migrations/0016_cablesegment_lashed_with.py +18 -0
  15. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/models.py +43 -0
  16. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/search.py +1 -1
  17. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/detail-map.min.js +2 -0
  18. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/detail-map.min.js.map +7 -0
  19. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/endpoint-markers.min.js +2 -0
  20. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/endpoint-markers.min.js.map +7 -0
  21. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/pathways-field.min.js +2 -0
  22. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/pathways-field.min.js.map +7 -0
  23. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/pathways-map.min.js +2 -0
  24. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/pathways-map.min.js.map +7 -0
  25. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/route-planner-map.min.js +2 -0
  26. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/dist/route-planner-map.min.js.map +7 -0
  27. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/geoman/leaflet-geoman.css +2 -0
  28. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/geoman/leaflet-geoman.js +2 -0
  29. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/leaflet/images/layers-2x.png +0 -0
  30. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/leaflet/images/layers.png +0 -0
  31. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/leaflet/images/marker-icon-2x.png +0 -0
  32. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/leaflet/images/marker-icon.png +0 -0
  33. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/leaflet/images/marker-shadow.png +0 -0
  34. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/leaflet/leaflet.css +661 -0
  35. netbox_pathways-0.2.0/netbox_pathways/static/netbox_pathways/vendor/leaflet/leaflet.js +6 -0
  36. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/tables.py +21 -0
  37. netbox_pathways-0.2.0/netbox_pathways/ui/__init__.py +0 -0
  38. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/ui/panels.py +39 -1
  39. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/views.py +8 -0
  40. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways.egg-info/PKG-INFO +49 -6
  41. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways.egg-info/SOURCES.txt +29 -0
  42. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways.egg-info/requires.txt +1 -1
  43. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/pyproject.toml +26 -24
  44. netbox_pathways-0.2.0/tests/test_field_additions.py +113 -0
  45. netbox_pathways-0.2.0/tests/test_graphql.py +325 -0
  46. netbox_pathways-0.2.0/tests/test_import_geodata.py +36 -0
  47. netbox_pathways-0.2.0/tests/test_lashed_with.py +94 -0
  48. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/api/__init__.py +0 -0
  49. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/api/external_geo.py +0 -0
  50. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/api/geo.py +0 -0
  51. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/api/traversal.py +0 -0
  52. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/api/urls.py +0 -0
  53. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/api/views.py +0 -0
  54. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/geo.py +0 -0
  55. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/graph.py +0 -0
  56. {netbox_pathways-0.1.0/netbox_pathways/management → netbox_pathways-0.2.0/netbox_pathways/graphql}/__init__.py +0 -0
  57. {netbox_pathways-0.1.0/netbox_pathways/management/commands → netbox_pathways-0.2.0/netbox_pathways/management}/__init__.py +0 -0
  58. {netbox_pathways-0.1.0/netbox_pathways/ui → netbox_pathways-0.2.0/netbox_pathways/management/commands}/__init__.py +0 -0
  59. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/management/commands/_geodata_worker.py +0 -0
  60. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/management/commands/generate_qgis_project.py +0 -0
  61. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/management/commands/generate_sample_data.py +0 -0
  62. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0001_initial.py +0 -0
  63. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0002_replace_owner_with_tenant.py +0 -0
  64. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0003_structure_optional_site_dimensions.py +0 -0
  65. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0004_circuit_geometry.py +0 -0
  66. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0005_replace_unique_together_with_constraints.py +0 -0
  67. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0006_remove_cablesegment_sequence_enter_exit.py +0 -0
  68. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0007_cable_routing_redesign.py +0 -0
  69. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0008_conduitbank_pathway_subclass.py +0 -0
  70. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0009_remove_conduit_unique_position_per_bank_and_more.py +0 -0
  71. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0010_structure_status.py +0 -0
  72. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0011_rename_name_to_label.py +0 -0
  73. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0012_add_filter_field_indexes.py +0 -0
  74. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0013_plannedroute.py +0 -0
  75. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/0014_plannedroute_parent_split.py +0 -0
  76. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/migrations/__init__.py +0 -0
  77. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/navigation.py +0 -0
  78. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/registry.py +0 -0
  79. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/route_engine.py +0 -0
  80. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/routing.py +0 -0
  81. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/signals.py +0 -0
  82. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/static/netbox_pathways/css/leaflet-theme.css +0 -0
  83. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/static/netbox_pathways/css/pathways-map.css +0 -0
  84. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/static/netbox_pathways/qgis/pathways.qml +0 -0
  85. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/static/netbox_pathways/qgis/structures.qml +0 -0
  86. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/static/netbox_pathways/vendor/MarkerCluster.Default.css +0 -0
  87. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/static/netbox_pathways/vendor/MarkerCluster.css +0 -0
  88. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/static/netbox_pathways/vendor/leaflet.markercluster.js +0 -0
  89. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/template_content.py +0 -0
  90. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/aerialspan.html +0 -0
  91. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/buttons/apply_route.html +0 -0
  92. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/buttons/replan_route.html +0 -0
  93. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/buttons/revert_split.html +0 -0
  94. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/buttons/split_route.html +0 -0
  95. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/buttons/view_in_map.html +0 -0
  96. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/cable_route_tab.html +0 -0
  97. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/cablesegment.html +0 -0
  98. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/conduit.html +0 -0
  99. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/conduitbank.html +0 -0
  100. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/conduitjunction.html +0 -0
  101. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/directburied.html +0 -0
  102. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/cable_add_segment_form.html +0 -0
  103. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/cable_route_finder_results.html +0 -0
  104. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/cable_routing_panel.html +0 -0
  105. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/cable_segment_table.html +0 -0
  106. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/connected_structures_panel.html +0 -0
  107. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/constraint_card.html +0 -0
  108. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/geo_map_panel.html +0 -0
  109. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/plannedroute_map_panel.html +0 -0
  110. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/inc/planner_results.html +0 -0
  111. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/innerduct.html +0 -0
  112. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/map.html +0 -0
  113. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/pathway.html +0 -0
  114. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/pathwaylocation.html +0 -0
  115. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/plannedroute.html +0 -0
  116. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/plannedroute_apply.html +0 -0
  117. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/plannedroute_split.html +0 -0
  118. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/pullsheet_detail.html +0 -0
  119. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/pullsheet_list.html +0 -0
  120. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/route_planner.html +0 -0
  121. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/sitegeometry.html +0 -0
  122. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/structure.html +0 -0
  123. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/templates/netbox_pathways/widgets/map_widget.html +0 -0
  124. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways/urls.py +0 -0
  125. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways.egg-info/dependency_links.txt +0 -0
  126. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/netbox_pathways.egg-info/top_level.txt +0 -0
  127. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/setup.cfg +0 -0
  128. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_adjacency.py +0 -0
  129. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_cable_segment.py +0 -0
  130. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_circuit_geometry.py +0 -0
  131. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_endpoint_validation.py +0 -0
  132. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_external_geo.py +0 -0
  133. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_geo_api.py +0 -0
  134. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_graph.py +0 -0
  135. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_map_view.py +0 -0
  136. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_planned_route.py +0 -0
  137. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_registry.py +0 -0
  138. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_route_engine.py +0 -0
  139. {netbox_pathways-0.1.0 → netbox_pathways-0.2.0}/tests/test_routing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-pathways
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: NetBox plugin for physical cable plant infrastructure documentation with GIS capabilities
5
5
  Author-email: Jonathan Senecal <contact@jonathansenecal.com>
6
6
  Project-URL: Homepage, https://github.com/jsenecal/netbox-pathways
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
19
19
  Requires-Dist: djangorestframework-gis>=1.2.0
20
20
  Requires-Dist: networkx>=3.0
21
21
  Provides-Extra: dev
22
- Requires-Dist: bumpver; extra == "dev"
22
+ Requires-Dist: bump-my-version; extra == "dev"
23
23
  Requires-Dist: pre-commit>=4.0.0; extra == "dev"
24
24
  Requires-Dist: pytest; extra == "dev"
25
25
  Requires-Dist: pytest-django>=4.5.0; extra == "dev"
@@ -30,6 +30,8 @@ Requires-Dist: zensical; extra == "docs"
30
30
 
31
31
  # netbox-pathways
32
32
 
33
+ > **Under active development -- alpha.** netbox-pathways is pre-1.0 and changing fast. Models, migrations, REST/GeoJSON endpoints, and configuration keys may break between releases without deprecation cycles. Pin an exact version in production, expect to read the [CHANGELOG](CHANGELOG.md) before every upgrade, and back up your database before running migrations. Issue reports and PRs are very welcome.
34
+
33
35
  > A NetBox plugin for documenting physical cable plant infrastructure with PostGIS integration. Track conduits, aerial spans, structures, and cable routing with geographic data, comparable to SmallWorld or ArcGIS with ArcFM for outside/inside plant documentation.
34
36
 
35
37
  [![PyPI](https://img.shields.io/pypi/v/netbox-pathways.svg)](https://pypi.org/project/netbox-pathways/)
@@ -37,6 +39,7 @@ Requires-Dist: zensical; extra == "docs"
37
39
  [![NetBox](https://img.shields.io/badge/NetBox-4.5%2B-success.svg)](https://github.com/netbox-community/netbox)
38
40
  [![CI](https://github.com/jsenecal/netbox-pathways/actions/workflows/ci.yml/badge.svg)](https://github.com/jsenecal/netbox-pathways/actions/workflows/ci.yml)
39
41
  [![codecov](https://codecov.io/gh/jsenecal/netbox-pathways/branch/main/graph/badge.svg)](https://codecov.io/gh/jsenecal/netbox-pathways)
42
+ [![Documentation](https://img.shields.io/badge/docs-jsenecal.github.io-blue)](https://jsenecal.github.io/netbox-pathways/)
40
43
  ![Status](https://img.shields.io/badge/status-alpha-orange)
41
44
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
42
45
 
@@ -61,18 +64,50 @@ Requires-Dist: zensical; extra == "docs"
61
64
 
62
65
  ## Installation
63
66
 
67
+ > NetBox runs on plain PostgreSQL by default. This plugin requires PostGIS, so installing it on an existing NetBox deployment means changing your database setup. The short version is below; see [PostGIS Setup](https://jsenecal.github.io/netbox-pathways/getting-started/postgis-setup/) in the docs for the full walkthrough (system libraries, container images, migrating an existing database).
68
+
69
+ ### 1. PostGIS prerequisites
70
+
71
+ Install the GIS system libraries on every NetBox host (web workers and `rq`), and PostGIS on the database server:
72
+
64
73
  ```bash
65
- pip install netbox-pathways
74
+ # Debian / Ubuntu (NetBox host)
75
+ sudo apt-get install -y gdal-bin libgdal-dev libgeos-dev libproj-dev binutils
76
+
77
+ # Database server: PostgreSQL 16+ with the PostGIS 3.4 package, then:
78
+ psql -d netbox -c "CREATE EXTENSION IF NOT EXISTS postgis;"
79
+ ```
80
+
81
+ ### 2. Switch NetBox to the PostGIS database backend
82
+
83
+ In `configuration.py`, the default `DATABASES` engine is plain PostgreSQL. Change `ENGINE` to the PostGIS backend (the standard `django.db.backends.postgresql` engine appears to work but fails the first time a geometry column is created):
84
+
85
+ ```python
86
+ DATABASES = {
87
+ "default": {
88
+ "ENGINE": "django.contrib.gis.db.backends.postgis", # was django.db.backends.postgresql
89
+ "NAME": "netbox",
90
+ "USER": "netbox",
91
+ "PASSWORD": "...",
92
+ "HOST": "localhost",
93
+ "PORT": "",
94
+ "CONN_MAX_AGE": 300,
95
+ },
96
+ }
66
97
  ```
67
98
 
68
- In your NetBox `configuration.py`:
99
+ ### 3. Install the plugin and configure it
100
+
101
+ ```bash
102
+ pip install netbox-pathways
103
+ ```
69
104
 
70
105
  ```python
71
106
  PLUGINS = ["netbox_pathways"]
72
107
 
73
108
  PLUGINS_CONFIG = {
74
109
  "netbox_pathways": {
75
- "srid": 3348, # REQUIRED -- your EPSG code (see warning below)
110
+ "srid": 3348, # REQUIRED -- your EPSG code (see SRID warning below)
76
111
  "map_center_lat": 45.5, # default map center latitude (optional)
77
112
  "map_center_lon": -73.5,# default map center longitude (optional)
78
113
  "map_zoom": 10, # default map zoom level (optional)
@@ -80,7 +115,7 @@ PLUGINS_CONFIG = {
80
115
  }
81
116
  ```
82
117
 
83
- Run migrations and restart:
118
+ ### 4. Migrate, collect static, restart
84
119
 
85
120
  ```bash
86
121
  cd /opt/netbox/netbox
@@ -89,6 +124,8 @@ python manage.py collectstatic --no-input
89
124
  sudo systemctl restart netbox netbox-rq
90
125
  ```
91
126
 
127
+ If `migrate` fails with errors mentioning `postgis`, `gdal`, or `geos`, the database backend is still on plain PostgreSQL or the GIS system libraries are missing on the NetBox host. The [PostGIS Setup](https://jsenecal.github.io/netbox-pathways/getting-started/postgis-setup/) page covers diagnosis and recovery.
128
+
92
129
  ## Configuration
93
130
 
94
131
  ### SRID is immutable after installation
@@ -125,11 +162,17 @@ Open the generated `.qgs` file in QGIS. Style files (`.qml`) ship under `static/
125
162
 
126
163
  All resources are exposed under `/api/plugins/pathways/`. GeoJSON variants live under `/api/plugins/pathways/geo/` for direct QGIS / OGR consumption. See [API Examples](https://jsenecal.github.io/netbox-pathways/developer/api-examples/) for full endpoint coverage.
127
164
 
165
+ ## GraphQL
166
+
167
+ Every Pathways model is exposed on NetBox's `/graphql/` endpoint (single and `_list` queries: `structure`, `structure_list`, `pathway`, `pathway_list`, `conduit`, `conduit_list`, etc.). Geometry fields are intentionally omitted -- query the GeoJSON REST endpoints for spatial data.
168
+
128
169
  ## Documentation
129
170
 
130
171
  Full documentation: **[jsenecal.github.io/netbox-pathways](https://jsenecal.github.io/netbox-pathways/)**
131
172
 
132
173
  - [Installation](https://jsenecal.github.io/netbox-pathways/getting-started/installation/)
174
+ - [PostGIS Setup](https://jsenecal.github.io/netbox-pathways/getting-started/postgis-setup/)
175
+ - [SRID Selection](https://jsenecal.github.io/netbox-pathways/getting-started/srid/)
133
176
  - [Configuration](https://jsenecal.github.io/netbox-pathways/getting-started/configuration/)
134
177
  - [Concepts](https://jsenecal.github.io/netbox-pathways/user-guide/concepts/)
135
178
  - [QGIS Integration](https://jsenecal.github.io/netbox-pathways/user-guide/qgis-integration/)
@@ -1,5 +1,7 @@
1
1
  # netbox-pathways
2
2
 
3
+ > **Under active development -- alpha.** netbox-pathways is pre-1.0 and changing fast. Models, migrations, REST/GeoJSON endpoints, and configuration keys may break between releases without deprecation cycles. Pin an exact version in production, expect to read the [CHANGELOG](CHANGELOG.md) before every upgrade, and back up your database before running migrations. Issue reports and PRs are very welcome.
4
+
3
5
  > A NetBox plugin for documenting physical cable plant infrastructure with PostGIS integration. Track conduits, aerial spans, structures, and cable routing with geographic data, comparable to SmallWorld or ArcGIS with ArcFM for outside/inside plant documentation.
4
6
 
5
7
  [![PyPI](https://img.shields.io/pypi/v/netbox-pathways.svg)](https://pypi.org/project/netbox-pathways/)
@@ -7,6 +9,7 @@
7
9
  [![NetBox](https://img.shields.io/badge/NetBox-4.5%2B-success.svg)](https://github.com/netbox-community/netbox)
8
10
  [![CI](https://github.com/jsenecal/netbox-pathways/actions/workflows/ci.yml/badge.svg)](https://github.com/jsenecal/netbox-pathways/actions/workflows/ci.yml)
9
11
  [![codecov](https://codecov.io/gh/jsenecal/netbox-pathways/branch/main/graph/badge.svg)](https://codecov.io/gh/jsenecal/netbox-pathways)
12
+ [![Documentation](https://img.shields.io/badge/docs-jsenecal.github.io-blue)](https://jsenecal.github.io/netbox-pathways/)
10
13
  ![Status](https://img.shields.io/badge/status-alpha-orange)
11
14
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
12
15
 
@@ -31,18 +34,50 @@
31
34
 
32
35
  ## Installation
33
36
 
37
+ > NetBox runs on plain PostgreSQL by default. This plugin requires PostGIS, so installing it on an existing NetBox deployment means changing your database setup. The short version is below; see [PostGIS Setup](https://jsenecal.github.io/netbox-pathways/getting-started/postgis-setup/) in the docs for the full walkthrough (system libraries, container images, migrating an existing database).
38
+
39
+ ### 1. PostGIS prerequisites
40
+
41
+ Install the GIS system libraries on every NetBox host (web workers and `rq`), and PostGIS on the database server:
42
+
34
43
  ```bash
35
- pip install netbox-pathways
44
+ # Debian / Ubuntu (NetBox host)
45
+ sudo apt-get install -y gdal-bin libgdal-dev libgeos-dev libproj-dev binutils
46
+
47
+ # Database server: PostgreSQL 16+ with the PostGIS 3.4 package, then:
48
+ psql -d netbox -c "CREATE EXTENSION IF NOT EXISTS postgis;"
49
+ ```
50
+
51
+ ### 2. Switch NetBox to the PostGIS database backend
52
+
53
+ In `configuration.py`, the default `DATABASES` engine is plain PostgreSQL. Change `ENGINE` to the PostGIS backend (the standard `django.db.backends.postgresql` engine appears to work but fails the first time a geometry column is created):
54
+
55
+ ```python
56
+ DATABASES = {
57
+ "default": {
58
+ "ENGINE": "django.contrib.gis.db.backends.postgis", # was django.db.backends.postgresql
59
+ "NAME": "netbox",
60
+ "USER": "netbox",
61
+ "PASSWORD": "...",
62
+ "HOST": "localhost",
63
+ "PORT": "",
64
+ "CONN_MAX_AGE": 300,
65
+ },
66
+ }
36
67
  ```
37
68
 
38
- In your NetBox `configuration.py`:
69
+ ### 3. Install the plugin and configure it
70
+
71
+ ```bash
72
+ pip install netbox-pathways
73
+ ```
39
74
 
40
75
  ```python
41
76
  PLUGINS = ["netbox_pathways"]
42
77
 
43
78
  PLUGINS_CONFIG = {
44
79
  "netbox_pathways": {
45
- "srid": 3348, # REQUIRED -- your EPSG code (see warning below)
80
+ "srid": 3348, # REQUIRED -- your EPSG code (see SRID warning below)
46
81
  "map_center_lat": 45.5, # default map center latitude (optional)
47
82
  "map_center_lon": -73.5,# default map center longitude (optional)
48
83
  "map_zoom": 10, # default map zoom level (optional)
@@ -50,7 +85,7 @@ PLUGINS_CONFIG = {
50
85
  }
51
86
  ```
52
87
 
53
- Run migrations and restart:
88
+ ### 4. Migrate, collect static, restart
54
89
 
55
90
  ```bash
56
91
  cd /opt/netbox/netbox
@@ -59,6 +94,8 @@ python manage.py collectstatic --no-input
59
94
  sudo systemctl restart netbox netbox-rq
60
95
  ```
61
96
 
97
+ If `migrate` fails with errors mentioning `postgis`, `gdal`, or `geos`, the database backend is still on plain PostgreSQL or the GIS system libraries are missing on the NetBox host. The [PostGIS Setup](https://jsenecal.github.io/netbox-pathways/getting-started/postgis-setup/) page covers diagnosis and recovery.
98
+
62
99
  ## Configuration
63
100
 
64
101
  ### SRID is immutable after installation
@@ -95,11 +132,17 @@ Open the generated `.qgs` file in QGIS. Style files (`.qml`) ship under `static/
95
132
 
96
133
  All resources are exposed under `/api/plugins/pathways/`. GeoJSON variants live under `/api/plugins/pathways/geo/` for direct QGIS / OGR consumption. See [API Examples](https://jsenecal.github.io/netbox-pathways/developer/api-examples/) for full endpoint coverage.
97
134
 
135
+ ## GraphQL
136
+
137
+ Every Pathways model is exposed on NetBox's `/graphql/` endpoint (single and `_list` queries: `structure`, `structure_list`, `pathway`, `pathway_list`, `conduit`, `conduit_list`, etc.). Geometry fields are intentionally omitted -- query the GeoJSON REST endpoints for spatial data.
138
+
98
139
  ## Documentation
99
140
 
100
141
  Full documentation: **[jsenecal.github.io/netbox-pathways](https://jsenecal.github.io/netbox-pathways/)**
101
142
 
102
143
  - [Installation](https://jsenecal.github.io/netbox-pathways/getting-started/installation/)
144
+ - [PostGIS Setup](https://jsenecal.github.io/netbox-pathways/getting-started/postgis-setup/)
145
+ - [SRID Selection](https://jsenecal.github.io/netbox-pathways/getting-started/srid/)
103
146
  - [Configuration](https://jsenecal.github.io/netbox-pathways/getting-started/configuration/)
104
147
  - [Concepts](https://jsenecal.github.io/netbox-pathways/user-guide/concepts/)
105
148
  - [QGIS Integration](https://jsenecal.github.io/netbox-pathways/user-guide/qgis-integration/)
@@ -1,6 +1,10 @@
1
+ import logging
2
+
1
3
  from netbox.plugins import PluginConfig
2
4
 
3
- __version__ = "0.1.0"
5
+ __version__ = "0.2.0"
6
+
7
+ logger = logging.getLogger(__name__)
4
8
 
5
9
 
6
10
  class NetBoxPathwaysConfig(PluginConfig):
@@ -11,6 +15,7 @@ class NetBoxPathwaysConfig(PluginConfig):
11
15
  author = "Jonathan Senecal"
12
16
  author_email = "contact@jonathansenecal.com"
13
17
  base_url = "pathways"
18
+ graphql_schema = "graphql.schema.schema"
14
19
  required_settings = ["srid"]
15
20
  default_settings = {
16
21
  "map_center_lat": 45.5017,
@@ -87,5 +92,7 @@ class NetBoxPathwaysConfig(PluginConfig):
87
92
  # Register signals
88
93
  from . import signals # noqa: F401
89
94
 
95
+ logger.info("%s plugin loaded", self.name)
96
+
90
97
 
91
98
  config = NetBoxPathwaysConfig
@@ -42,6 +42,7 @@ class StructureSerializer(NetBoxModelSerializer):
42
42
  structure_type = ChoiceField(choices=StructureTypeChoices, required=False, allow_blank=True)
43
43
  site = SiteSerializer(nested=True, required=False, allow_null=True)
44
44
  tenant = TenantSerializer(nested=True, required=False, allow_null=True)
45
+ installed_by = TenantSerializer(nested=True, required=False, allow_null=True)
45
46
  no_pathways = drf_serializers.SerializerMethodField(read_only=True)
46
47
  description = drf_serializers.SerializerMethodField(read_only=True)
47
48
 
@@ -63,7 +64,9 @@ class StructureSerializer(NetBoxModelSerializer):
63
64
  "depth",
64
65
  "elevation",
65
66
  "installation_date",
67
+ "commissioned_date",
66
68
  "tenant",
69
+ "installed_by",
67
70
  "access_notes",
68
71
  "comments",
69
72
  "tags",
@@ -119,6 +122,7 @@ class ConduitBankSerializer(NetBoxModelSerializer):
119
122
  configuration = ChoiceField(choices=ConduitBankConfigChoices, required=False, allow_blank=True)
120
123
  encasement_type = ChoiceField(choices=EncasementTypeChoices, required=False, allow_blank=True)
121
124
  tenant = TenantSerializer(nested=True, required=False, allow_null=True)
125
+ installed_by = TenantSerializer(nested=True, required=False, allow_null=True)
122
126
 
123
127
  class Meta:
124
128
  model = ConduitBank
@@ -133,12 +137,14 @@ class ConduitBankSerializer(NetBoxModelSerializer):
133
137
  "start_face",
134
138
  "end_face",
135
139
  "tenant",
140
+ "installed_by",
136
141
  "path",
137
142
  "length",
138
143
  "configuration",
139
144
  "total_conduits",
140
145
  "encasement_type",
141
146
  "installation_date",
147
+ "commissioned_date",
142
148
  "comments",
143
149
  "tags",
144
150
  "created",
@@ -157,6 +163,7 @@ class PathwaySerializer(NetBoxModelSerializer):
157
163
  start_location = LocationSerializer(nested=True, required=False, allow_null=True)
158
164
  end_location = LocationSerializer(nested=True, required=False, allow_null=True)
159
165
  tenant = TenantSerializer(nested=True, required=False, allow_null=True)
166
+ installed_by = TenantSerializer(nested=True, required=False, allow_null=True)
160
167
  cables_routed = drf_serializers.IntegerField(read_only=True)
161
168
 
162
169
  class Meta:
@@ -174,9 +181,11 @@ class PathwaySerializer(NetBoxModelSerializer):
174
181
  "start_location",
175
182
  "end_location",
176
183
  "tenant",
184
+ "installed_by",
177
185
  "length",
178
186
  "cables_routed",
179
187
  "installation_date",
188
+ "commissioned_date",
180
189
  "comments",
181
190
  "tags",
182
191
  "created",
@@ -196,6 +205,7 @@ class ConduitSerializer(NetBoxModelSerializer):
196
205
  end_location = LocationSerializer(nested=True, required=False, allow_null=True)
197
206
  conduit_bank = ConduitBankSerializer(nested=True, required=False, allow_null=True)
198
207
  tenant = TenantSerializer(nested=True, required=False, allow_null=True)
208
+ installed_by = TenantSerializer(nested=True, required=False, allow_null=True)
199
209
  cables_routed = drf_serializers.IntegerField(read_only=True)
200
210
 
201
211
  class Meta:
@@ -220,9 +230,12 @@ class ConduitSerializer(NetBoxModelSerializer):
220
230
  "bank_position",
221
231
  "start_junction",
222
232
  "end_junction",
233
+ "tenant",
234
+ "installed_by",
223
235
  "length",
224
236
  "cables_routed",
225
237
  "installation_date",
238
+ "commissioned_date",
226
239
  "comments",
227
240
  "tags",
228
241
  "created",
@@ -241,6 +254,7 @@ class AerialSpanSerializer(NetBoxModelSerializer):
241
254
  start_location = LocationSerializer(nested=True, required=False, allow_null=True)
242
255
  end_location = LocationSerializer(nested=True, required=False, allow_null=True)
243
256
  tenant = TenantSerializer(nested=True, required=False, allow_null=True)
257
+ installed_by = TenantSerializer(nested=True, required=False, allow_null=True)
244
258
  cables_routed = drf_serializers.IntegerField(read_only=True)
245
259
 
246
260
  class Meta:
@@ -263,9 +277,12 @@ class AerialSpanSerializer(NetBoxModelSerializer):
263
277
  "messenger_size",
264
278
  "wind_loading",
265
279
  "ice_loading",
280
+ "tenant",
281
+ "installed_by",
266
282
  "length",
267
283
  "cables_routed",
268
284
  "installation_date",
285
+ "commissioned_date",
269
286
  "comments",
270
287
  "tags",
271
288
  "created",
@@ -283,6 +300,7 @@ class DirectBuriedSerializer(NetBoxModelSerializer):
283
300
  start_location = LocationSerializer(nested=True, required=False, allow_null=True)
284
301
  end_location = LocationSerializer(nested=True, required=False, allow_null=True)
285
302
  tenant = TenantSerializer(nested=True, required=False, allow_null=True)
303
+ installed_by = TenantSerializer(nested=True, required=False, allow_null=True)
286
304
  cables_routed = drf_serializers.IntegerField(read_only=True)
287
305
 
288
306
  class Meta:
@@ -303,9 +321,12 @@ class DirectBuriedSerializer(NetBoxModelSerializer):
303
321
  "warning_tape",
304
322
  "tracer_wire",
305
323
  "armor_type",
324
+ "tenant",
325
+ "installed_by",
306
326
  "length",
307
327
  "cables_routed",
308
328
  "installation_date",
329
+ "commissioned_date",
309
330
  "comments",
310
331
  "tags",
311
332
  "created",
@@ -323,6 +344,7 @@ class InnerductSerializer(NetBoxModelSerializer):
323
344
  start_location = LocationSerializer(nested=True, required=False, allow_null=True)
324
345
  end_location = LocationSerializer(nested=True, required=False, allow_null=True)
325
346
  parent_conduit = ConduitSerializer(nested=True, required=False, allow_null=True)
347
+ installed_by = TenantSerializer(nested=True, required=False, allow_null=True)
326
348
  cables_routed = drf_serializers.IntegerField(read_only=True)
327
349
 
328
350
  class Meta:
@@ -343,9 +365,11 @@ class InnerductSerializer(NetBoxModelSerializer):
343
365
  "size",
344
366
  "color",
345
367
  "position",
368
+ "installed_by",
346
369
  "length",
347
370
  "cables_routed",
348
371
  "installation_date",
372
+ "commissioned_date",
349
373
  "comments",
350
374
  "tags",
351
375
  "created",
@@ -415,6 +439,12 @@ class CableSegmentSerializer(NetBoxModelSerializer):
415
439
  )
416
440
  cable = CableSerializer(nested=True, required=False, allow_null=True)
417
441
  pathway = PathwaySerializer(nested=True, required=False, allow_null=True)
442
+ lashed_with = drf_serializers.PrimaryKeyRelatedField(
443
+ many=True,
444
+ required=False,
445
+ queryset=CableSegment.objects.all(),
446
+ help_text="Other cable segments lashed together with this one (symmetrical).",
447
+ )
418
448
 
419
449
  class Meta:
420
450
  model = CableSegment
@@ -426,6 +456,7 @@ class CableSegmentSerializer(NetBoxModelSerializer):
426
456
  "cable",
427
457
  "pathway",
428
458
  "sequence",
459
+ "lashed_with",
429
460
  "comments",
430
461
  "tags",
431
462
  "created",
@@ -9,6 +9,7 @@ class StructureStatusChoices(ChoiceSet):
9
9
  STATUS_CONSTRUCTION = "construction"
10
10
  STATUS_DECOMMISSIONING = "decommissioning"
11
11
  STATUS_RETIRED = "retired"
12
+ STATUS_ABANDONED = "abandoned"
12
13
 
13
14
  CHOICES = [
14
15
  (STATUS_PLANNED, "Planned", "cyan"),
@@ -16,6 +17,7 @@ class StructureStatusChoices(ChoiceSet):
16
17
  (STATUS_CONSTRUCTION, "Under Construction", "blue"),
17
18
  (STATUS_DECOMMISSIONING, "Decommissioning", "yellow"),
18
19
  (STATUS_RETIRED, "Retired", "red"),
20
+ (STATUS_ABANDONED, "Abandoned in place", "gray"),
19
21
  ]
20
22
 
21
23
 
@@ -41,7 +41,7 @@ class StructureFilterForm(NetBoxModelFilterSetForm):
41
41
  fieldsets = (
42
42
  FieldSet("q", "filter_id", "tag"),
43
43
  FieldSet("status", "structure_type", "site_id", name="Attributes"),
44
- FieldSet("tenant_id", name="Tenant"),
44
+ FieldSet("tenant_id", "installed_by_id", name="Tenant"),
45
45
  )
46
46
  status = forms.MultipleChoiceField(choices=StructureStatusChoices, required=False)
47
47
  structure_type = forms.MultipleChoiceField(choices=StructureTypeChoices, required=False)
@@ -56,6 +56,12 @@ class StructureFilterForm(NetBoxModelFilterSetForm):
56
56
  null_option="None",
57
57
  label="Tenant",
58
58
  )
59
+ installed_by_id = DynamicModelMultipleChoiceField(
60
+ queryset=Tenant.objects.all(),
61
+ required=False,
62
+ null_option="None",
63
+ label="Installed by",
64
+ )
59
65
  tag = TagFilterField(model)
60
66
 
61
67
 
@@ -65,7 +71,7 @@ class PathwayFilterForm(NetBoxModelFilterSetForm):
65
71
  FieldSet("q", "filter_id", "tag"),
66
72
  FieldSet("pathway_type", name="Attributes"),
67
73
  FieldSet("start_structure_id", "end_structure_id", "start_location_id", "end_location_id", name="Endpoints"),
68
- FieldSet("tenant_id", name="Tenant"),
74
+ FieldSet("tenant_id", "installed_by_id", name="Tenant"),
69
75
  )
70
76
  pathway_type = forms.MultipleChoiceField(choices=PathwayTypeChoices, required=False)
71
77
  start_structure_id = DynamicModelMultipleChoiceField(
@@ -94,6 +100,12 @@ class PathwayFilterForm(NetBoxModelFilterSetForm):
94
100
  null_option="None",
95
101
  label="Tenant",
96
102
  )
103
+ installed_by_id = DynamicModelMultipleChoiceField(
104
+ queryset=Tenant.objects.all(),
105
+ required=False,
106
+ null_option="None",
107
+ label="Installed by",
108
+ )
97
109
  tag = TagFilterField(model)
98
110
 
99
111
 
@@ -213,7 +225,7 @@ class ConduitBankFilterForm(NetBoxModelFilterSetForm):
213
225
  FieldSet("q", "filter_id", "tag"),
214
226
  FieldSet("configuration", "encasement_type", "start_face", "end_face", name="Attributes"),
215
227
  FieldSet("start_structure_id", "end_structure_id", name="Endpoints"),
216
- FieldSet("tenant_id", name="Tenant"),
228
+ FieldSet("tenant_id", "installed_by_id", name="Tenant"),
217
229
  )
218
230
  configuration = forms.MultipleChoiceField(choices=ConduitBankConfigChoices, required=False)
219
231
  encasement_type = forms.MultipleChoiceField(choices=EncasementTypeChoices, required=False)
@@ -235,6 +247,12 @@ class ConduitBankFilterForm(NetBoxModelFilterSetForm):
235
247
  null_option="None",
236
248
  label="Tenant",
237
249
  )
250
+ installed_by_id = DynamicModelMultipleChoiceField(
251
+ queryset=Tenant.objects.all(),
252
+ required=False,
253
+ null_option="None",
254
+ label="Installed by",
255
+ )
238
256
  tag = TagFilterField(model)
239
257
 
240
258
 
@@ -261,7 +279,7 @@ class CableSegmentFilterForm(NetBoxModelFilterSetForm):
261
279
  model = CableSegment
262
280
  fieldsets = (
263
281
  FieldSet("q", "filter_id", "tag"),
264
- FieldSet("cable_id", "pathway_id", name="Attributes"),
282
+ FieldSet("cable_id", "pathway_id", "lashed_with_id", name="Attributes"),
265
283
  )
266
284
  cable_id = DynamicModelMultipleChoiceField(
267
285
  queryset=Cable.objects.all(),
@@ -273,6 +291,11 @@ class CableSegmentFilterForm(NetBoxModelFilterSetForm):
273
291
  required=False,
274
292
  label="Pathway",
275
293
  )
294
+ lashed_with_id = DynamicModelMultipleChoiceField(
295
+ queryset=CableSegment.objects.all(),
296
+ required=False,
297
+ label="Lashed with segment",
298
+ )
276
299
  tag = TagFilterField(model)
277
300
 
278
301
 
@@ -4,6 +4,7 @@ from dcim.models import Cable, Location, Site
4
4
  from django.db.models import Q
5
5
  from netbox.filtersets import NetBoxModelFilterSet
6
6
  from tenancy.filtersets import TenancyFilterSet
7
+ from tenancy.models import Tenant
7
8
  from utilities.filters import MultiValueCharFilter, MultiValueNumberFilter
8
9
 
9
10
  from .choices import (
@@ -58,6 +59,12 @@ class StructureFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
58
59
  distinct=False,
59
60
  label="Site (slug)",
60
61
  )
62
+ installed_by_id = django_filters.ModelMultipleChoiceFilter(
63
+ field_name="installed_by",
64
+ queryset=Tenant.objects.all(),
65
+ distinct=False,
66
+ label="Installed by (ID)",
67
+ )
61
68
  height = MultiValueNumberFilter()
62
69
  width = MultiValueNumberFilter()
63
70
  length = MultiValueNumberFilter()
@@ -74,7 +81,7 @@ class StructureFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
74
81
 
75
82
  class Meta:
76
83
  model = Structure
77
- fields = ["id", "installation_date"]
84
+ fields = ["id", "installation_date", "commissioned_date"]
78
85
 
79
86
  def filter_occupied(self, queryset, name, value):
80
87
  occupied_pws = CableSegment.objects.values_list("pathway_id", flat=True)
@@ -165,6 +172,12 @@ class PathwayFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
165
172
  label="End Location (slug)",
166
173
  )
167
174
  length = MultiValueNumberFilter()
175
+ installed_by_id = django_filters.ModelMultipleChoiceFilter(
176
+ field_name="installed_by",
177
+ queryset=Tenant.objects.all(),
178
+ distinct=False,
179
+ label="Installed by (ID)",
180
+ )
168
181
  occupied = django_filters.BooleanFilter(
169
182
  method="filter_occupied",
170
183
  label="Occupied (has routed cables)",
@@ -172,7 +185,7 @@ class PathwayFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
172
185
 
173
186
  class Meta:
174
187
  model = Pathway
175
- fields = ["id", "installation_date"]
188
+ fields = ["id", "installation_date", "commissioned_date"]
176
189
 
177
190
  def filter_occupied(self, queryset, name, value):
178
191
  occupied_pws = CableSegment.objects.values_list("pathway_id", flat=True)
@@ -244,7 +257,7 @@ class ConduitFilterSet(NetBoxModelFilterSet):
244
257
 
245
258
  class Meta:
246
259
  model = Conduit
247
- fields = ["id", "installation_date"]
260
+ fields = ["id", "installation_date", "commissioned_date"]
248
261
 
249
262
  def search(self, queryset, name, value):
250
263
  if not value.strip():
@@ -292,7 +305,7 @@ class AerialSpanFilterSet(NetBoxModelFilterSet):
292
305
 
293
306
  class Meta:
294
307
  model = AerialSpan
295
- fields = ["id", "installation_date"]
308
+ fields = ["id", "installation_date", "commissioned_date"]
296
309
 
297
310
  def search(self, queryset, name, value):
298
311
  if not value.strip():
@@ -334,7 +347,7 @@ class DirectBuriedFilterSet(NetBoxModelFilterSet):
334
347
 
335
348
  class Meta:
336
349
  model = DirectBuried
337
- fields = ["id", "installation_date"]
350
+ fields = ["id", "installation_date", "commissioned_date"]
338
351
 
339
352
  def search(self, queryset, name, value):
340
353
  if not value.strip():
@@ -356,7 +369,7 @@ class InnerductFilterSet(NetBoxModelFilterSet):
356
369
 
357
370
  class Meta:
358
371
  model = Innerduct
359
- fields = ["id", "installation_date"]
372
+ fields = ["id", "installation_date", "commissioned_date"]
360
373
 
361
374
  def search(self, queryset, name, value):
362
375
  if not value.strip():
@@ -403,7 +416,7 @@ class ConduitBankFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
403
416
 
404
417
  class Meta:
405
418
  model = ConduitBank
406
- fields = ["id", "installation_date"]
419
+ fields = ["id", "installation_date", "commissioned_date"]
407
420
 
408
421
  def search(self, queryset, name, value):
409
422
  if not value.strip():
@@ -456,6 +469,12 @@ class CableSegmentFilterSet(NetBoxModelFilterSet):
456
469
  distinct=False,
457
470
  label="Pathway (ID)",
458
471
  )
472
+ lashed_with_id = django_filters.ModelMultipleChoiceFilter(
473
+ field_name="lashed_with",
474
+ queryset=CableSegment.objects.all(),
475
+ distinct=False,
476
+ label="Lashed with segment (ID)",
477
+ )
459
478
  sequence = MultiValueNumberFilter()
460
479
 
461
480
  class Meta: