nautobot 2.4.2__py3-none-any.whl → 2.4.4__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 (267) hide show
  1. nautobot/apps/filters.py +2 -0
  2. nautobot/circuits/filters.py +1 -1
  3. nautobot/circuits/templates/circuits/inc/circuit_termination.html +1 -1
  4. nautobot/circuits/tests/integration/test_circuit.py +135 -0
  5. nautobot/circuits/tests/test_models.py +5 -3
  6. nautobot/circuits/views.py +4 -1
  7. nautobot/cloud/api/views.py +3 -3
  8. nautobot/cloud/filters.py +3 -6
  9. nautobot/cloud/tests/test_filters.py +21 -0
  10. nautobot/core/admin.py +2 -0
  11. nautobot/core/constants.py +0 -1
  12. nautobot/core/forms/__init__.py +2 -0
  13. nautobot/core/forms/forms.py +2 -1
  14. nautobot/core/forms/widgets.py +8 -0
  15. nautobot/core/jobs/__init__.py +2 -1
  16. nautobot/core/management/commands/generate_performance_test_endpoints.py +271 -0
  17. nautobot/core/models/utils.py +6 -1
  18. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  19. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  20. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  21. nautobot/core/templates/generic/object_create.html +5 -0
  22. nautobot/core/templates/generic/object_delete.html +1 -1
  23. nautobot/core/templates/generic/object_detail.html +1 -1
  24. nautobot/core/templates/generic/object_edit.html +1 -1
  25. nautobot/core/templates/inc/javascript.html +3 -0
  26. nautobot/core/templates/widgets/clearable_file.html +5 -0
  27. nautobot/core/templatetags/helpers.py +3 -3
  28. nautobot/core/templatetags/ui_framework.py +20 -4
  29. nautobot/core/testing/forms.py +1 -1
  30. nautobot/core/testing/integration.py +37 -7
  31. nautobot/core/tests/test_api.py +1 -1
  32. nautobot/core/tests/test_commands.py +31 -0
  33. nautobot/core/tests/test_graphql.py +3 -3
  34. nautobot/core/tests/test_jobs.py +4 -1
  35. nautobot/core/tests/test_utils.py +17 -2
  36. nautobot/core/ui/object_detail.py +1 -1
  37. nautobot/core/utils/lookup.py +12 -1
  38. nautobot/core/views/generic.py +9 -1
  39. nautobot/core/views/mixins.py +9 -1
  40. nautobot/dcim/api/serializers.py +36 -0
  41. nautobot/dcim/api/views.py +12 -11
  42. nautobot/dcim/elevations.py +17 -4
  43. nautobot/dcim/factory.py +9 -1
  44. nautobot/dcim/filters/__init__.py +27 -1
  45. nautobot/dcim/forms.py +16 -7
  46. nautobot/dcim/models/devices.py +12 -7
  47. nautobot/dcim/signals.py +26 -0
  48. nautobot/dcim/templates/dcim/cable_trace.html +4 -4
  49. nautobot/dcim/templates/dcim/consoleport.html +14 -4
  50. nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
  51. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
  52. nautobot/dcim/templates/dcim/frontport.html +7 -2
  53. nautobot/dcim/templates/dcim/interface.html +9 -4
  54. nautobot/dcim/templates/dcim/powerfeed.html +8 -3
  55. nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
  56. nautobot/dcim/templates/dcim/powerport.html +14 -4
  57. nautobot/dcim/templates/dcim/rearport.html +7 -2
  58. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
  59. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
  60. nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
  61. nautobot/dcim/tests/test_api.py +176 -0
  62. nautobot/dcim/tests/test_filters.py +56 -3
  63. nautobot/dcim/tests/test_models.py +41 -1
  64. nautobot/dcim/views.py +24 -14
  65. nautobot/extras/api/mixins.py +1 -1
  66. nautobot/extras/api/views.py +4 -4
  67. nautobot/extras/filters/__init__.py +4 -0
  68. nautobot/extras/forms/forms.py +4 -0
  69. nautobot/extras/jobs.py +8 -1
  70. nautobot/extras/models/datasources.py +7 -3
  71. nautobot/extras/plugins/__init__.py +26 -1
  72. nautobot/extras/templates/extras/inc/jobresult.html +12 -13
  73. nautobot/extras/templates/extras/job.html +1 -0
  74. nautobot/extras/templates/extras/objectchange.html +28 -12
  75. nautobot/extras/tests/test_api.py +16 -15
  76. nautobot/extras/tests/test_dynamicgroups.py +14 -0
  77. nautobot/extras/tests/test_filters.py +2 -0
  78. nautobot/extras/tests/test_plugins.py +32 -1
  79. nautobot/extras/tests/test_views.py +209 -11
  80. nautobot/extras/utils.py +30 -0
  81. nautobot/extras/views.py +32 -14
  82. nautobot/ipam/api/serializers.py +7 -8
  83. nautobot/ipam/api/views.py +5 -5
  84. nautobot/ipam/factory.py +27 -8
  85. nautobot/ipam/filters.py +67 -29
  86. nautobot/ipam/formfields.py +51 -0
  87. nautobot/ipam/forms.py +15 -7
  88. nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
  89. nautobot/ipam/models.py +63 -5
  90. nautobot/ipam/tables.py +21 -7
  91. nautobot/ipam/tests/test_api.py +107 -66
  92. nautobot/ipam/tests/test_filters.py +145 -5
  93. nautobot/ipam/tests/test_views.py +15 -2
  94. nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
  95. nautobot/project-static/css/base.css +11 -0
  96. nautobot/project-static/css/dark.css +2 -1
  97. nautobot/project-static/docs/apps/index.html +1 -1
  98. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  99. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  100. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
  101. nautobot/project-static/docs/development/apps/api/models/graphql.html +9 -13
  102. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
  103. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -5
  104. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
  105. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
  106. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
  107. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
  108. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  109. nautobot/project-static/docs/development/apps/api/testing.html +0 -6
  110. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
  111. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
  112. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
  113. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
  114. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
  115. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
  116. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
  117. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
  118. nautobot/project-static/docs/development/apps/index.html +2 -35
  119. nautobot/project-static/docs/development/apps/migration/code-updates.html +7 -6
  120. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
  121. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  122. nautobot/project-static/docs/development/core/application-registry.html +0 -6
  123. nautobot/project-static/docs/development/core/best-practices.html +1 -28
  124. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  125. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +65 -11
  126. nautobot/project-static/docs/development/core/getting-started.html +14 -18
  127. nautobot/project-static/docs/development/core/homepage.html +0 -3
  128. nautobot/project-static/docs/development/core/index.html +1 -1
  129. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +3 -3
  130. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  131. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  132. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  133. nautobot/project-static/docs/development/core/settings.html +1 -1
  134. nautobot/project-static/docs/development/core/style-guide.html +4 -9
  135. nautobot/project-static/docs/development/core/templates.html +0 -3
  136. nautobot/project-static/docs/development/core/testing.html +0 -9
  137. nautobot/project-static/docs/development/jobs/index.html +11 -30
  138. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -2
  139. nautobot/project-static/docs/index.html +3 -2
  140. nautobot/project-static/docs/objects.inv +0 -0
  141. nautobot/project-static/docs/overview/application_stack.html +2 -20
  142. nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
  143. nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
  144. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  145. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  146. nautobot/project-static/docs/release-notes/version-1.4.html +17 -17
  147. nautobot/project-static/docs/release-notes/version-1.5.html +8 -8
  148. nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
  149. nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
  150. nautobot/project-static/docs/release-notes/version-2.1.html +7 -7
  151. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  152. nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
  153. nautobot/project-static/docs/release-notes/version-2.4.html +379 -0
  154. nautobot/project-static/docs/requirements.txt +1 -1
  155. nautobot/project-static/docs/search/search_index.json +1 -1
  156. nautobot/project-static/docs/sitemap.xml +290 -290
  157. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  158. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  159. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  160. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  161. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -13
  162. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +5 -5
  163. nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -18
  164. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  165. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  166. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +15 -15
  167. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
  168. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  169. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
  170. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  171. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +7 -10
  172. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -12
  173. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/security/index.html +1 -1
  175. nautobot/project-static/docs/user-guide/administration/security/notices.html +1 -0
  176. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
  177. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  178. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  179. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +12 -9
  180. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
  181. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
  182. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
  183. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
  184. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
  185. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
  186. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
  187. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
  188. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
  189. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
  205. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
  206. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
  208. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
  209. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
  210. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +15 -15
  211. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  212. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
  213. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
  214. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
  215. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -27
  216. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
  217. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -6
  218. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
  219. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
  220. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
  221. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
  222. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +2 -2
  223. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +6 -6
  224. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
  225. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
  226. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
  227. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
  228. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
  229. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
  230. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
  231. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
  232. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
  233. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
  234. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
  235. nautobot/project-static/js/dropdown.js +28 -0
  236. nautobot/project-static/js/editor.js +292 -0
  237. nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
  238. nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  239. nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
  240. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
  241. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
  242. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
  243. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
  244. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
  245. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
  246. nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
  247. nautobot/tenancy/filters/__init__.py +3 -5
  248. nautobot/tenancy/forms.py +9 -0
  249. nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
  250. nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
  251. nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
  252. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
  253. nautobot/tenancy/tests/test_filters.py +10 -0
  254. nautobot/tenancy/tests/test_views.py +5 -1
  255. nautobot/tenancy/urls.py +7 -79
  256. nautobot/tenancy/views.py +51 -80
  257. nautobot/virtualization/views.py +0 -1
  258. nautobot/wireless/api/serializers.py +6 -1
  259. nautobot/wireless/api/views.py +3 -3
  260. nautobot/wireless/tables.py +9 -4
  261. nautobot/wireless/tests/test_api.py +5 -9
  262. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/METADATA +9 -9
  263. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/RECORD +267 -246
  264. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/LICENSE.txt +0 -0
  265. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/NOTICE +0 -0
  266. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/WHEEL +0 -0
  267. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/entry_points.txt +0 -0
@@ -820,17 +820,33 @@ class DynamicGroupTestCase(
820
820
  return super()._get_queryset().filter(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # TODO
821
821
 
822
822
  def test_get_object_with_permission(self):
823
- instance = self._get_queryset().first()
823
+ location_ct = ContentType.objects.get_for_model(Location)
824
+ instance = self._get_queryset().exclude(content_type=location_ct).first()
824
825
  # Add view permissions for the group's members:
825
- self.add_permissions(get_permission_for_model(instance.content_type.model_class(), "view"))
826
+ self.add_permissions(
827
+ get_permission_for_model(instance.content_type.model_class(), "view"), "extras.view_dynamicgroup"
828
+ )
826
829
 
827
- response = super().test_get_object_with_permission()
830
+ response = self.client.get(instance.get_absolute_url())
831
+ self.assertHttpStatus(response, 200)
828
832
 
829
833
  response_body = extract_page_body(response.content.decode(response.charset))
830
834
  # Check that the "members" table in the detail view includes all appropriate member objects
831
835
  for member in instance.members:
832
836
  self.assertIn(str(member.pk), response_body)
833
837
 
838
+ # Test accessing DynamicGroup detail view with a different content type, more specifically, TreeModel
839
+ # https://github.com/nautobot/nautobot/issues/6806
840
+ tree_model_dg = DynamicGroup.objects.create(name="DG 4", content_type=location_ct)
841
+ # Add view permissions for the group's members:
842
+ self.add_permissions(get_permission_for_model(tree_model_dg.content_type.model_class(), "view"))
843
+ response = self.client.get(tree_model_dg.get_absolute_url())
844
+ self.assertHttpStatus(response, 200)
845
+ response_body = extract_page_body(response.content.decode(response.charset))
846
+ # Check that the "members" table in the detail view includes all appropriate member objects
847
+ for member in tree_model_dg.members:
848
+ self.assertIn(str(member.pk), response_body)
849
+
834
850
  def test_get_object_with_constrained_permission(self):
835
851
  instance = self._get_queryset().first()
836
852
  # Add view permission for one of the group's members but not the others:
@@ -1164,6 +1180,13 @@ class GitRepositoryTestCase(
1164
1180
  self.form_data = form_data
1165
1181
  super().test_edit_object_with_constrained_permission()
1166
1182
 
1183
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1184
+ def test_view_when_no_sync_job_result_exists(self):
1185
+ instance = self._get_queryset().first()
1186
+ response = self.client.get(reverse("extras:gitrepository_result", kwargs={"pk": instance.pk}))
1187
+ self.assertEqual(response.status_code, 200)
1188
+ self.assertEqual(response.context["result"], {})
1189
+
1167
1190
  def test_post_sync_repo_anonymous(self):
1168
1191
  self.client.logout()
1169
1192
  url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
@@ -1306,19 +1329,18 @@ class SavedViewTest(ModelViewTestCase):
1306
1329
 
1307
1330
  model = SavedView
1308
1331
 
1309
- def get_view_url_for_saved_view(self, saved_view, action="detail"):
1332
+ def get_view_url_for_saved_view(self, saved_view=None, action="detail"):
1310
1333
  """
1311
1334
  Since saved view detail url redirects, we need to manually construct its detail url
1312
1335
  to test the content of its response.
1313
1336
  """
1314
- view = saved_view.view
1315
- pk = saved_view.pk
1337
+ url = ""
1316
1338
 
1317
- if action == "detail":
1318
- url = reverse(view) + f"?saved_view={pk}"
1319
- elif action == "edit":
1339
+ if action == "detail" and saved_view:
1340
+ url = reverse(saved_view.view) + f"?saved_view={saved_view.pk}"
1341
+ elif action == "edit" and saved_view:
1320
1342
  url = saved_view.get_absolute_url() + "update-config/"
1321
- else:
1343
+ elif action == "create":
1322
1344
  url = reverse("extras:savedview_add")
1323
1345
 
1324
1346
  return url
@@ -1411,7 +1433,14 @@ class SavedViewTest(ModelViewTestCase):
1411
1433
 
1412
1434
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1413
1435
  def test_update_saved_view_as_owner(self):
1414
- instance = self._get_queryset().first()
1436
+ view_name = "dcim:location_list"
1437
+ instance = SavedView.objects.create(
1438
+ name="Location Saved View",
1439
+ owner=self.user,
1440
+ view=view_name,
1441
+ is_global_default=True,
1442
+ )
1443
+
1415
1444
  update_query_strings = ["per_page=12", "&status=active", "&name=new_name_filter", "&sort=name"]
1416
1445
  update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1417
1446
  # Try update the saved view with the same user as the owner of the saved view
@@ -1543,6 +1572,62 @@ class SavedViewTest(ModelViewTestCase):
1543
1572
  # Assert that Location List View got redirected to Saved View set as user default
1544
1573
  self.assertBodyContains(response, "<strong>User Location Default View</strong>", html=True)
1545
1574
 
1575
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1576
+ def test_filtered_view_precedes_global_default(self):
1577
+ view_name = "dcim:location_list"
1578
+ # Global saved view that will show Floor type locations only.
1579
+ SavedView.objects.create(
1580
+ name="Global Location Default View",
1581
+ owner=self.user,
1582
+ view=view_name,
1583
+ is_global_default=True,
1584
+ config={
1585
+ "filter_params": {
1586
+ "location_type": ["Floor"],
1587
+ }
1588
+ },
1589
+ )
1590
+ response = self.client.get(reverse(view_name) + "?location_type=Campus", follow=True)
1591
+ # Assert that the user is not redirected to the global default view
1592
+ # But instead redirected to the filtered view
1593
+ self.assertNotIn(
1594
+ "<strong>Global Location Default View</strong>",
1595
+ extract_page_body(response.content.decode(response.charset)),
1596
+ )
1597
+
1598
+ # Floor type locations (Floor-<number>) should not be visible in the response
1599
+ self.assertNotIn(
1600
+ "Floor-",
1601
+ extract_page_body(response.content.decode(response.charset)),
1602
+ )
1603
+
1604
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1605
+ def test_filtered_view_precedes_user_default(self):
1606
+ view_name = "dcim:location_list"
1607
+ # User saved view that will show Floor type locations only.
1608
+ sv = SavedView.objects.create(
1609
+ name="User Location Default View",
1610
+ owner=self.user,
1611
+ view=view_name,
1612
+ config={
1613
+ "filter_params": {
1614
+ "location_type": ["Floor"],
1615
+ }
1616
+ },
1617
+ )
1618
+ UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
1619
+ response = self.client.get(reverse(view_name) + "?location_type=Campus", follow=True)
1620
+ # Assert that the user is not redirected to the user default view
1621
+ # But instead redirected to the filtered view
1622
+ self.assertNotIn(
1623
+ "<strong>User Location Default View</strong>", extract_page_body(response.content.decode(response.charset))
1624
+ )
1625
+ # Floor type locations (Floor-<number>) should not be visible in the response
1626
+ self.assertNotIn(
1627
+ "Floor-",
1628
+ extract_page_body(response.content.decode(response.charset)),
1629
+ )
1630
+
1546
1631
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1547
1632
  def test_is_shared(self):
1548
1633
  view_name = "dcim:location_list"
@@ -1568,6 +1653,119 @@ class SavedViewTest(ModelViewTestCase):
1568
1653
  self.assertIn(str(sv_shared.pk), response_body, msg=response_body)
1569
1654
  self.assertNotIn(str(sv_not_shared.pk), response_body, msg=response_body)
1570
1655
 
1656
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1657
+ def test_create_saved_views_contain_boolean_filter_params(self):
1658
+ """
1659
+ Test the entire Save View workflow from creating a Saved View to rendering the View with boolean filter parameters.
1660
+ """
1661
+ with self.subTest("Create job Saved View with boolean filter parameters"):
1662
+ view_name = "extras:job_list"
1663
+ app_label = view_name.split(":")[0]
1664
+ model_name = view_name.split(":")[1].split("_")[0]
1665
+ self.add_permissions(f"{app_label}.view_{model_name}")
1666
+ create_query_strings = [
1667
+ "&hidden=True",
1668
+ ]
1669
+ create_url = self.get_view_url_for_saved_view(action="create")
1670
+ sv_name = "Hidden Jobs"
1671
+ request = {
1672
+ "path": create_url,
1673
+ "data": post_data({"name": sv_name, "view": f"{view_name}", "params": "".join(create_query_strings)}),
1674
+ }
1675
+ self.assertHttpStatus(self.client.post(**request), 302)
1676
+ instance = SavedView.objects.get(name=sv_name)
1677
+ hidden_job = Job.objects.get(name="Example hidden job")
1678
+ hidden_job.description = "I should not show in the UI!"
1679
+ hidden_job.save()
1680
+ self.assertEqual(instance.config["filter_params"]["hidden"], "True")
1681
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1682
+ # Assert that Job List View rendered with the boolean filter parameter without error
1683
+ self.assertHttpStatus(response, 200)
1684
+ response_body = extract_page_body(response.content.decode(response.charset))
1685
+ self.assertIn(str(instance.pk), response_body, msg=response_body)
1686
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1687
+ # This is the description
1688
+ self.assertBodyContains(response, "I should not show in the UI!", html=True)
1689
+
1690
+ with self.subTest("Create device Saved View with boolean filter parameters"):
1691
+ view_name = "dcim:device_list"
1692
+ app_label = view_name.split(":")[0]
1693
+ model_name = view_name.split(":")[1].split("_")[0]
1694
+ self.add_permissions(f"{app_label}.view_{model_name}")
1695
+ create_query_strings = [
1696
+ "&per_page=12",
1697
+ "&has_primary_ip=True",
1698
+ "&sort=name",
1699
+ ]
1700
+ create_url = self.get_view_url_for_saved_view(action="create")
1701
+ sv_name = "Devices with primary ips"
1702
+ request = {
1703
+ "path": create_url,
1704
+ "data": post_data({"name": sv_name, "view": f"{view_name}", "params": "".join(create_query_strings)}),
1705
+ }
1706
+ self.assertHttpStatus(self.client.post(**request), 302)
1707
+ instance = SavedView.objects.get(name=sv_name)
1708
+ self.assertEqual(instance.config["pagination_count"], 12)
1709
+ self.assertEqual(instance.config["filter_params"]["has_primary_ip"], "True")
1710
+ self.assertEqual(instance.config["sort_order"], ["name"])
1711
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1712
+ # Assert that Job List View rendered with the boolean filter parameter without error
1713
+ self.assertHttpStatus(response, 200)
1714
+ response_body = extract_page_body(response.content.decode(response.charset))
1715
+ self.assertIn(str(instance.pk), response_body, msg=response_body)
1716
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1717
+
1718
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1719
+ def test_update_saved_view_contain_boolean_filter_params(self):
1720
+ with self.subTest("Update job Saved View with boolean filter parameters"):
1721
+ view_name = "extras:job_list"
1722
+ sv_name = "Non-hidden jobs"
1723
+ instance = SavedView.objects.create(
1724
+ name=sv_name,
1725
+ owner=self.user,
1726
+ view=view_name,
1727
+ )
1728
+ update_query_strings = ["hidden=False"]
1729
+ update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1730
+ # Try update the saved view with the same user as the owner of the saved view
1731
+ instance.owner.is_active = True
1732
+ instance.owner.save()
1733
+ self.client.force_login(instance.owner)
1734
+ response = self.client.get(update_url)
1735
+ self.assertHttpStatus(response, 302)
1736
+ instance.refresh_from_db()
1737
+ self.assertEqual(instance.config["filter_params"]["hidden"], "False")
1738
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1739
+ # Assert that Job List View rendered with the boolean filter parameter without error
1740
+ self.assertHttpStatus(response, 200)
1741
+ response_body = extract_page_body(response.content.decode(response.charset))
1742
+ self.assertNotIn("Example hidden job", response_body, msg=response_body)
1743
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1744
+
1745
+ with self.subTest("Update device Saved View with boolean filter parameters"):
1746
+ view_name = "dcim:device_list"
1747
+ sv_name = "Devices with no primary ips"
1748
+ instance = SavedView.objects.create(
1749
+ name=sv_name,
1750
+ owner=self.user,
1751
+ view=view_name,
1752
+ )
1753
+ update_query_strings = ["has_primary_ip=False"]
1754
+ update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1755
+ # Try update the saved view with the same user as the owner of the saved view
1756
+ instance.owner.is_active = True
1757
+ instance.owner.save()
1758
+ self.client.force_login(instance.owner)
1759
+ response = self.client.get(update_url)
1760
+ self.assertHttpStatus(response, 302)
1761
+ instance.refresh_from_db()
1762
+ self.assertEqual(instance.config["filter_params"]["has_primary_ip"], "False")
1763
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1764
+ # Assert that Job List View rendered with the boolean filter parameter without error
1765
+ self.assertHttpStatus(response, 200)
1766
+ response_body = extract_page_body(response.content.decode(response.charset))
1767
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1768
+
1571
1769
 
1572
1770
  # Not a full-fledged PrimaryObjectViewTestCase as there's no BulkEditView for Secrets
1573
1771
  class SecretTestCase(
nautobot/extras/utils.py CHANGED
@@ -22,9 +22,12 @@ import redis.exceptions
22
22
 
23
23
  from nautobot.core.choices import ColorChoices
24
24
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
25
+ from nautobot.core.exceptions import FilterSetFieldNotFound
25
26
  from nautobot.core.models.managers import TagsManager
26
27
  from nautobot.core.models.utils import find_models_with_matching_fields
27
28
  from nautobot.core.utils.data import is_uuid
29
+ from nautobot.core.utils.lookup import get_filterset_for_model, get_model_for_view_name
30
+ from nautobot.core.utils.requests import is_single_choice_field
28
31
  from nautobot.extras.choices import DynamicGroupTypeChoices, JobQueueTypeChoices, ObjectChangeActionChoices
29
32
  from nautobot.extras.constants import (
30
33
  CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL,
@@ -880,3 +883,30 @@ def bulk_delete_with_bulk_change_logging(qs, batch_size=1000):
880
883
  finally:
881
884
  change_context.defer_object_changes = False
882
885
  change_context.reset_deferred_object_changes()
886
+
887
+
888
+ def fixup_filterset_query_params(param_dict, view_name, non_filter_params):
889
+ """
890
+ Called before saving query filter parameters to a SavedView's config. This function will format
891
+ single value query parameters to be saved as a single values instead of lists of singles values.
892
+
893
+ Args:
894
+ param_dict (dict): key-value pairs of query parameters.
895
+ view_name (str): The name of the view that the saved view is associated with. "dcim:location_list" for example.
896
+ non_filter_params (list): List of non-query parameters that should not be formatted.
897
+ """
898
+ model = get_model_for_view_name(view_name)
899
+ try:
900
+ filterset_class = get_filterset_for_model(model)
901
+ except TypeError:
902
+ return param_dict
903
+
904
+ filterset = filterset_class()
905
+
906
+ for filter_field, value in param_dict.items():
907
+ try:
908
+ if filter_field not in non_filter_params and is_single_choice_field(filterset, filter_field):
909
+ param_dict[filter_field] = value[0]
910
+ except FilterSetFieldNotFound:
911
+ pass
912
+ return param_dict
nautobot/extras/views.py CHANGED
@@ -26,6 +26,7 @@ from rest_framework.permissions import IsAuthenticated
26
26
 
27
27
  from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
28
28
  from nautobot.core.events import publish_event
29
+ from nautobot.core.exceptions import FilterSetFieldNotFound
29
30
  from nautobot.core.forms import restrict_form_fields
30
31
  from nautobot.core.models.querysets import count_related
31
32
  from nautobot.core.models.utils import pretty_print_query, serialize_object_v2
@@ -36,12 +37,13 @@ from nautobot.core.ui.object_detail import ObjectDetailContent, ObjectFieldsPane
36
37
  from nautobot.core.utils.config import get_settings_or_config
37
38
  from nautobot.core.utils.lookup import (
38
39
  get_filterset_for_model,
40
+ get_model_for_view_name,
39
41
  get_route_for_model,
40
42
  get_table_class_string_from_view_name,
41
43
  get_table_for_model,
42
44
  )
43
45
  from nautobot.core.utils.permissions import get_permission_for_model
44
- from nautobot.core.utils.requests import normalize_querydict
46
+ from nautobot.core.utils.requests import is_single_choice_field, normalize_querydict
45
47
  from nautobot.core.views import generic, viewsets
46
48
  from nautobot.core.views.mixins import (
47
49
  GetReturnURLMixin,
@@ -69,7 +71,7 @@ from nautobot.dcim.tables import (
69
71
  VirtualDeviceContextTable,
70
72
  )
71
73
  from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
72
- from nautobot.extras.utils import get_base_template, get_job_queue, get_worker_count
74
+ from nautobot.extras.utils import fixup_filterset_query_params, get_base_template, get_job_queue, get_worker_count
73
75
  from nautobot.ipam.models import IPAddress, Prefix, VLAN
74
76
  from nautobot.ipam.tables import IPAddressTable, PrefixTable, VLANTable
75
77
  from nautobot.virtualization.models import VirtualMachine, VMInterface
@@ -720,8 +722,13 @@ class DynamicGroupView(generic.ObjectView):
720
722
 
721
723
  if table_class is not None:
722
724
  # Members table (for display on Members nav tab)
725
+ if hasattr(members, "without_tree_fields"):
726
+ members = members.without_tree_fields()
723
727
  members_table = table_class(
724
- members.restrict(request.user, "view"), orderable=False, exclude=["dynamic_group_count"]
728
+ members.restrict(request.user, "view"),
729
+ orderable=False,
730
+ exclude=["dynamic_group_count"],
731
+ hide_hierarchy_ui=True,
725
732
  )
726
733
  paginate = {
727
734
  "paginator_class": EnhancedPaginator,
@@ -1158,6 +1165,9 @@ class GitRepositoryResultView(generic.ObjectView):
1158
1165
  def get_extra_context(self, request, instance):
1159
1166
  job_result = instance.get_latest_sync()
1160
1167
 
1168
+ if job_result is None:
1169
+ job_result = {}
1170
+
1161
1171
  return {
1162
1172
  "result": job_result,
1163
1173
  "base_template": "extras/gitrepository.html",
@@ -1265,9 +1275,10 @@ class JobListView(generic.ObjectListView):
1265
1275
  def alter_queryset(self, request):
1266
1276
  queryset = super().alter_queryset(request)
1267
1277
  # Default to hiding "hidden" and non-installed jobs
1268
- if "hidden" not in request.GET:
1278
+ filter_params = self.get_filter_params(request)
1279
+ if "hidden" not in filter_params:
1269
1280
  queryset = queryset.filter(hidden=False)
1270
- if "installed" not in request.GET:
1281
+ if "installed" not in filter_params:
1271
1282
  queryset = queryset.filter(installed=True)
1272
1283
  return queryset
1273
1284
 
@@ -1806,15 +1817,23 @@ class SavedViewUIViewSet(
1806
1817
  if sort_order:
1807
1818
  sv.config["sort_order"] = sort_order
1808
1819
 
1820
+ model = get_model_for_view_name(sv.view)
1821
+ filterset_class = get_filterset_for_model(model)
1822
+ filterset = filterset_class()
1809
1823
  filter_params = {}
1810
1824
  for key in request.GET:
1811
1825
  if key in self.non_filter_params:
1812
1826
  continue
1813
- # TODO: this is fragile, other single-value filters will also be unhappy if given a list
1814
- if key == "q":
1815
- filter_params[key] = request.GET.get(key)
1816
- else:
1817
- filter_params[key] = request.GET.getlist(key)
1827
+ try:
1828
+ if is_single_choice_field(filterset, key):
1829
+ filter_params[key] = request.GET.getlist(key)[0]
1830
+ except FilterSetFieldNotFound:
1831
+ continue
1832
+ try:
1833
+ if not is_single_choice_field(filterset, key):
1834
+ filter_params[key] = request.GET.getlist(key)
1835
+ except FilterSetFieldNotFound:
1836
+ continue
1818
1837
 
1819
1838
  if filter_params:
1820
1839
  sv.config["filter_params"] = filter_params
@@ -1839,14 +1858,14 @@ class SavedViewUIViewSet(
1839
1858
  and the name of the new SavedView from request.POST to create a new SavedView.
1840
1859
  """
1841
1860
  name = request.POST.get("name")
1861
+ view_name = request.POST.get("view")
1842
1862
  is_shared = request.POST.get("is_shared", False)
1843
1863
  if is_shared:
1844
1864
  is_shared = True
1845
1865
  params = request.POST.get("params", "")
1866
+ param_dict = fixup_filterset_query_params(parse_qs(params), view_name, self.non_filter_params)
1846
1867
 
1847
- param_dict = parse_qs(params)
1848
-
1849
- single_value_params = ["saved_view", "table_changes_pending", "all_filters_removed", "q", "per_page"]
1868
+ single_value_params = ["saved_view", "table_changes_pending", "all_filters_removed", "per_page"]
1850
1869
  for key in param_dict.keys():
1851
1870
  if key in single_value_params:
1852
1871
  param_dict[key] = param_dict[key][0]
@@ -1855,7 +1874,6 @@ class SavedViewUIViewSet(
1855
1874
  derived_instance = None
1856
1875
  if derived_view_pk:
1857
1876
  derived_instance = self.get_queryset().get(pk=derived_view_pk)
1858
- view_name = request.POST.get("view")
1859
1877
  try:
1860
1878
  reverse(view_name)
1861
1879
  except NoReverseMatch:
@@ -63,14 +63,13 @@ class VRFDeviceAssignmentSerializer(ValidatedModelSerializer):
63
63
  validators = []
64
64
 
65
65
  def validate(self, attrs):
66
- if attrs.get("device"):
67
- validator = UniqueTogetherValidator(queryset=VRFDeviceAssignment.objects.all(), fields=("device", "vrf"))
68
- validator(attrs, self)
69
- if attrs.get("virtual_machine"):
70
- validator = UniqueTogetherValidator(
71
- queryset=VRFDeviceAssignment.objects.all(), fields=("virtual_machine", "vrf")
72
- )
73
- validator(attrs, self)
66
+ foreign_key_fields = ["device", "virtual_machine", "virtual_device_context"]
67
+ for foreign_key in foreign_key_fields:
68
+ if attrs.get(foreign_key):
69
+ validator = UniqueTogetherValidator(
70
+ queryset=VRFDeviceAssignment.objects.all(), fields=(foreign_key, "vrf")
71
+ )
72
+ validator(attrs, self)
74
73
  return super().validate(attrs)
75
74
 
76
75
 
@@ -13,7 +13,7 @@ from nautobot.core.constants import MAX_PAGE_SIZE_DEFAULT, PAGINATE_COUNT_DEFAUL
13
13
  from nautobot.core.models.querysets import count_related
14
14
  from nautobot.core.utils.config import get_settings_or_config
15
15
  from nautobot.dcim.models import Location
16
- from nautobot.extras.api.views import NautobotModelViewSet
16
+ from nautobot.extras.api.views import ModelViewSet, NautobotModelViewSet
17
17
  from nautobot.ipam import filters
18
18
  from nautobot.ipam.api import serializers
19
19
  from nautobot.ipam.models import (
@@ -55,13 +55,13 @@ class VRFViewSet(NautobotModelViewSet):
55
55
  filterset_class = filters.VRFFilterSet
56
56
 
57
57
 
58
- class VRFDeviceAssignmentViewSet(NautobotModelViewSet):
58
+ class VRFDeviceAssignmentViewSet(ModelViewSet):
59
59
  queryset = VRFDeviceAssignment.objects.all()
60
60
  serializer_class = serializers.VRFDeviceAssignmentSerializer
61
61
  filterset_class = filters.VRFDeviceAssignmentFilterSet
62
62
 
63
63
 
64
- class VRFPrefixAssignmentViewSet(NautobotModelViewSet):
64
+ class VRFPrefixAssignmentViewSet(ModelViewSet):
65
65
  queryset = VRFPrefixAssignment.objects.all()
66
66
  serializer_class = serializers.VRFPrefixAssignmentSerializer
67
67
  filterset_class = filters.VRFPrefixAssignmentFilterSet
@@ -323,7 +323,7 @@ class PrefixViewSet(NautobotModelViewSet):
323
323
  return Response(serializer.data)
324
324
 
325
325
 
326
- class PrefixLocationAssignmentViewSet(NautobotModelViewSet):
326
+ class PrefixLocationAssignmentViewSet(ModelViewSet):
327
327
  queryset = PrefixLocationAssignment.objects.all()
328
328
  serializer_class = serializers.PrefixLocationAssignmentSerializer
329
329
  filterset_class = filters.PrefixLocationAssignmentFilterSet
@@ -581,7 +581,7 @@ class VLANViewSet(NautobotModelViewSet):
581
581
  raise self.LocationIncompatibleLegacyBehavior from e
582
582
 
583
583
 
584
- class VLANLocationAssignmentViewSet(NautobotModelViewSet):
584
+ class VLANLocationAssignmentViewSet(ModelViewSet):
585
585
  queryset = VLANLocationAssignment.objects.all()
586
586
  serializer_class = serializers.VLANLocationAssignmentSerializer
587
587
  filterset_class = filters.VLANLocationAssignmentFilterSet
nautobot/ipam/factory.py CHANGED
@@ -15,7 +15,7 @@ from nautobot.core.factory import (
15
15
  random_instance,
16
16
  UniqueFaker,
17
17
  )
18
- from nautobot.dcim.models import Location
18
+ from nautobot.dcim.models import Location, VirtualDeviceContext
19
19
  from nautobot.extras.models import Role, Status
20
20
  from nautobot.ipam.choices import PrefixTypeChoices
21
21
  from nautobot.ipam.models import IPAddress, Namespace, Prefix, RIR, RouteTarget, VLAN, VLANGroup, VRF
@@ -127,6 +127,24 @@ class VRFFactory(PrimaryModelFactory):
127
127
  else:
128
128
  self.export_targets.set(get_random_instances(RouteTarget))
129
129
 
130
+ @factory.post_generation
131
+ def prefixes(self, create, extracted, **kwargs):
132
+ if create:
133
+ if extracted:
134
+ self.prefixes.set(extracted)
135
+ else:
136
+ self.prefixes.set(
137
+ get_random_instances(lambda: Prefix.objects.filter(namespace=self.namespace), minimum=0)
138
+ )
139
+
140
+ @factory.post_generation
141
+ def virtual_device_contexts(self, create, extracted, **kwargs):
142
+ if create:
143
+ if extracted:
144
+ self.virtual_device_contexts.set(extracted)
145
+ else:
146
+ self.virtual_device_contexts.set(get_random_instances(VirtualDeviceContext))
147
+
130
148
 
131
149
  class VLANGroupFactory(OrganizationalModelFactory):
132
150
  class Meta:
@@ -295,7 +313,6 @@ class PrefixFactory(PrimaryModelFactory):
295
313
  has_role = NautobotBoolIterator()
296
314
  has_tenant = NautobotBoolIterator()
297
315
  has_vlan = NautobotBoolIterator()
298
- # has_vrf = NautobotBoolIterator()
299
316
  is_ipv6 = NautobotBoolIterator()
300
317
 
301
318
  prefix = factory.Maybe(
@@ -321,12 +338,6 @@ class PrefixFactory(PrimaryModelFactory):
321
338
  None,
322
339
  )
323
340
  namespace = random_instance(Namespace, allow_null=False)
324
- # TODO: Update for M2M tests
325
- # vrf = factory.Maybe(
326
- # "has_vrf",
327
- # factory.SubFactory(VRFGetOrCreateFactory, tenant=factory.SelfAttribute("..tenant")),
328
- # None,
329
- # )
330
341
  rir = factory.Maybe("has_rir", random_instance(RIR, allow_null=False), None)
331
342
  date_allocated = factory.Maybe("has_date_allocated", factory.Faker("date_time", tzinfo=datetime.timezone.utc), None)
332
343
 
@@ -343,6 +354,14 @@ class PrefixFactory(PrimaryModelFactory):
343
354
  )
344
355
  )
345
356
 
357
+ @factory.post_generation
358
+ def vrfs(self, create, extracted, **kwargs):
359
+ if create:
360
+ if extracted:
361
+ self.vrfs.set(extracted)
362
+ else:
363
+ self.vrfs.set(get_random_instances(lambda: VRF.objects.filter(namespace=self.namespace), minimum=0))
364
+
346
365
  @factory.post_generation
347
366
  def children(self, create, extracted, **kwargs):
348
367
  """Creates child prefixes and ip addresses within the prefix IP space.