nautobot 2.3.16__py3-none-any.whl → 2.4.0b1__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 nautobot might be problematic. Click here for more details.

Files changed (675) hide show
  1. nautobot/__init__.py +15 -0
  2. nautobot/apps/api.py +0 -2
  3. nautobot/apps/config.py +32 -3
  4. nautobot/apps/events.py +19 -0
  5. nautobot/apps/exceptions.py +0 -2
  6. nautobot/apps/ui.py +44 -9
  7. nautobot/apps/utils.py +0 -8
  8. nautobot/apps/views.py +2 -0
  9. nautobot/circuits/navigation.py +0 -57
  10. nautobot/circuits/tables.py +1 -2
  11. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -71
  12. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -64
  13. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +40 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +26 -0
  15. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -76
  16. nautobot/circuits/tests/integration/test_relationships.py +33 -24
  17. nautobot/circuits/tests/test_filters.py +4 -8
  18. nautobot/circuits/views.py +143 -26
  19. nautobot/cloud/factory.py +4 -1
  20. nautobot/cloud/models.py +1 -1
  21. nautobot/cloud/tests/test_filters.py +5 -4
  22. nautobot/core/api/fields.py +5 -5
  23. nautobot/core/api/metadata.py +28 -256
  24. nautobot/core/api/pagination.py +3 -2
  25. nautobot/core/api/renderers.py +3 -0
  26. nautobot/core/api/serializers.py +24 -244
  27. nautobot/core/api/urls.py +3 -4
  28. nautobot/core/api/utils.py +0 -62
  29. nautobot/core/api/views.py +48 -158
  30. nautobot/core/apps/__init__.py +22 -578
  31. nautobot/core/celery/__init__.py +13 -0
  32. nautobot/core/celery/log.py +4 -4
  33. nautobot/core/celery/schedulers.py +48 -3
  34. nautobot/core/cli/__init__.py +8 -0
  35. nautobot/core/constants.py +7 -0
  36. nautobot/core/events/__init__.py +116 -0
  37. nautobot/core/events/base.py +27 -0
  38. nautobot/core/events/exceptions.py +10 -0
  39. nautobot/core/events/redis_broker.py +48 -0
  40. nautobot/core/events/syslog_broker.py +19 -0
  41. nautobot/core/exceptions.py +0 -6
  42. nautobot/core/filters.py +16 -21
  43. nautobot/core/fixtures/user-data.json +59 -0
  44. nautobot/core/forms/fields.py +53 -8
  45. nautobot/core/forms/utils.py +2 -1
  46. nautobot/core/graphql/schema.py +3 -1
  47. nautobot/core/graphql/types.py +1 -1
  48. nautobot/core/jobs/__init__.py +4 -4
  49. nautobot/core/jobs/cleanup.py +13 -49
  50. nautobot/core/jobs/groups.py +1 -1
  51. nautobot/core/management/commands/generate_test_data.py +21 -0
  52. nautobot/core/management/commands/validate_models.py +1 -1
  53. nautobot/core/middleware.py +16 -0
  54. nautobot/core/models/__init__.py +1 -1
  55. nautobot/core/models/fields.py +11 -7
  56. nautobot/core/models/query_functions.py +2 -2
  57. nautobot/core/models/tree_queries.py +3 -6
  58. nautobot/core/settings.py +44 -7
  59. nautobot/core/settings.yaml +86 -8
  60. nautobot/core/tables.py +15 -65
  61. nautobot/core/tasks.py +1 -1
  62. nautobot/core/templates/components/button/default.html +7 -0
  63. nautobot/core/templates/components/button/dropdown.html +20 -0
  64. nautobot/core/templates/components/layout/one_over_two.html +19 -0
  65. nautobot/core/templates/components/layout/two_over_one.html +19 -0
  66. nautobot/core/templates/components/panel/body_content_data_table.html +27 -0
  67. nautobot/core/templates/components/panel/body_content_objects_table.html +4 -0
  68. nautobot/core/templates/components/panel/body_content_tags.html +6 -0
  69. nautobot/core/templates/components/panel/body_content_text.html +12 -0
  70. nautobot/core/templates/components/panel/body_wrapper_generic.html +3 -0
  71. nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +3 -0
  72. nautobot/core/templates/components/panel/body_wrapper_table.html +3 -0
  73. nautobot/core/templates/components/panel/footer_contacts_table.html +20 -0
  74. nautobot/core/templates/components/panel/footer_content_table.html +14 -0
  75. nautobot/core/templates/components/panel/grouping_toggle.html +14 -0
  76. nautobot/core/templates/components/panel/header_extra_content_table.html +3 -0
  77. nautobot/core/templates/components/panel/panel.html +16 -0
  78. nautobot/core/templates/components/panel/stats_panel_body.html +8 -0
  79. nautobot/core/templates/components/tab/content_wrapper.html +3 -0
  80. nautobot/core/templates/components/tab/label_wrapper.html +5 -0
  81. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +3 -0
  82. nautobot/core/templates/generic/object_retrieve.html +28 -17
  83. nautobot/core/templates/inc/computed_fields/panel_data.html +4 -7
  84. nautobot/core/templates/inc/custom_fields/panel.html +2 -2
  85. nautobot/core/templates/inc/custom_fields/panel_data.html +4 -7
  86. nautobot/core/templates/inc/footer.html +1 -0
  87. nautobot/core/templates/inc/media.html +0 -3
  88. nautobot/core/templates/inc/nav_menu.html +1 -1
  89. nautobot/core/templates/inc/relationships_panel.html +1 -1
  90. nautobot/core/templates/nautobot_config.py.j2 +3 -3
  91. nautobot/core/templates/panel_table.html +12 -0
  92. nautobot/core/templates/search.html +0 -7
  93. nautobot/core/templates/utilities/render_jinja2.html +117 -0
  94. nautobot/core/templatetags/helpers.py +101 -12
  95. nautobot/core/templatetags/ui_framework.py +40 -0
  96. nautobot/core/testing/api.py +23 -128
  97. nautobot/core/testing/context.py +18 -0
  98. nautobot/core/testing/filters.py +41 -58
  99. nautobot/core/testing/mixins.py +2 -7
  100. nautobot/core/testing/views.py +25 -123
  101. nautobot/core/tests/integration/test_app_home.py +1 -0
  102. nautobot/core/tests/integration/test_app_navbar.py +1 -0
  103. nautobot/core/tests/integration/test_filters.py +2 -0
  104. nautobot/core/tests/integration/test_home.py +1 -0
  105. nautobot/core/tests/integration/test_navbar.py +1 -0
  106. nautobot/core/tests/integration/test_view_authentication.py +1 -2
  107. nautobot/core/tests/nautobot_config.py +198 -0
  108. nautobot/core/tests/runner.py +3 -3
  109. nautobot/core/tests/test_api.py +82 -201
  110. nautobot/core/tests/test_csv.py +3 -25
  111. nautobot/core/tests/test_events.py +214 -0
  112. nautobot/core/tests/test_jinja_filters.py +1 -0
  113. nautobot/core/tests/test_jobs.py +84 -13
  114. nautobot/core/tests/test_navigations.py +7 -241
  115. nautobot/core/tests/test_templatetags_helpers.py +16 -0
  116. nautobot/core/tests/test_ui.py +150 -0
  117. nautobot/core/tests/test_utils.py +0 -25
  118. nautobot/core/tests/test_views.py +123 -31
  119. nautobot/core/ui/__init__.py +0 -0
  120. nautobot/core/ui/base.py +11 -0
  121. nautobot/core/ui/choices.py +44 -0
  122. nautobot/core/ui/homepage.py +167 -0
  123. nautobot/core/ui/nav.py +279 -0
  124. nautobot/core/ui/object_detail.py +1841 -0
  125. nautobot/core/ui/utils.py +36 -0
  126. nautobot/core/urls.py +4 -9
  127. nautobot/core/utils/config.py +30 -3
  128. nautobot/core/utils/lookup.py +20 -13
  129. nautobot/core/views/__init__.py +6 -1
  130. nautobot/core/views/generic.py +47 -52
  131. nautobot/core/views/mixins.py +15 -25
  132. nautobot/core/views/paginator.py +8 -5
  133. nautobot/core/views/renderers.py +3 -3
  134. nautobot/core/views/utils.py +11 -0
  135. nautobot/core/wsgi.py +3 -3
  136. nautobot/dcim/api/serializers.py +80 -179
  137. nautobot/dcim/api/urls.py +5 -0
  138. nautobot/dcim/api/views.py +17 -4
  139. nautobot/dcim/apps.py +1 -0
  140. nautobot/dcim/choices.py +28 -0
  141. nautobot/dcim/factory.py +58 -0
  142. nautobot/dcim/filters/__init__.py +197 -24
  143. nautobot/dcim/forms.py +203 -12
  144. nautobot/dcim/graphql/types.py +2 -2
  145. nautobot/dcim/migrations/0063_interfacevdcassignment_virtualdevicecontext_and_more.py +165 -0
  146. nautobot/dcim/migrations/0064_virtualdevicecontext_status_data_migration.py +28 -0
  147. nautobot/dcim/migrations/0065_controller_capabilities_and_more.py +29 -0
  148. nautobot/dcim/migrations/0066_controllermanageddevicegroup_radio_profiles_and_more.py +33 -0
  149. nautobot/dcim/models/__init__.py +4 -0
  150. nautobot/dcim/models/device_component_templates.py +2 -2
  151. nautobot/dcim/models/device_components.py +20 -22
  152. nautobot/dcim/models/devices.py +173 -4
  153. nautobot/dcim/models/locations.py +3 -3
  154. nautobot/dcim/models/power.py +5 -6
  155. nautobot/dcim/models/racks.py +6 -6
  156. nautobot/dcim/navigation.py +25 -224
  157. nautobot/dcim/signals.py +44 -0
  158. nautobot/dcim/tables/__init__.py +5 -3
  159. nautobot/dcim/tables/devices.py +96 -2
  160. nautobot/dcim/tables/devicetypes.py +2 -2
  161. nautobot/dcim/templates/dcim/controller/base.html +10 -0
  162. nautobot/dcim/templates/dcim/controller_create.html +1 -0
  163. nautobot/dcim/templates/dcim/controller_retrieve.html +5 -1
  164. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +25 -0
  165. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +66 -0
  166. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +46 -0
  167. nautobot/dcim/templates/dcim/device/base.html +6 -42
  168. nautobot/dcim/templates/dcim/device/wireless.html +73 -0
  169. nautobot/dcim/templates/dcim/device.html +3 -1
  170. nautobot/dcim/templates/dcim/interface.html +1 -0
  171. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  172. nautobot/dcim/templates/dcim/locationtype.html +0 -107
  173. nautobot/dcim/templates/dcim/locationtype_retrieve.html +8 -0
  174. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +76 -0
  175. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +34 -0
  176. nautobot/dcim/tests/test_api.py +172 -61
  177. nautobot/dcim/tests/test_filters.py +171 -109
  178. nautobot/dcim/tests/test_forms.py +2 -51
  179. nautobot/dcim/tests/test_graphql.py +0 -52
  180. nautobot/dcim/tests/test_models.py +126 -4
  181. nautobot/dcim/tests/test_signals.py +1 -0
  182. nautobot/dcim/tests/test_views.py +103 -11
  183. nautobot/dcim/urls.py +72 -27
  184. nautobot/dcim/utils.py +2 -2
  185. nautobot/dcim/views.py +369 -62
  186. nautobot/extras/api/customfields.py +2 -2
  187. nautobot/extras/api/serializers.py +91 -75
  188. nautobot/extras/api/urls.py +4 -0
  189. nautobot/extras/api/views.py +78 -15
  190. nautobot/extras/choices.py +13 -0
  191. nautobot/extras/constants.py +0 -1
  192. nautobot/extras/context_managers.py +23 -6
  193. nautobot/extras/datasources/git.py +4 -1
  194. nautobot/extras/factory.py +27 -0
  195. nautobot/extras/filters/__init__.py +59 -0
  196. nautobot/extras/forms/forms.py +125 -30
  197. nautobot/extras/forms/mixins.py +3 -11
  198. nautobot/extras/graphql/types.py +25 -1
  199. nautobot/extras/group_sync.py +3 -3
  200. nautobot/extras/health_checks.py +2 -1
  201. nautobot/extras/jobs.py +62 -26
  202. nautobot/extras/management/__init__.py +1 -0
  203. nautobot/extras/management/commands/runjob.py +7 -79
  204. nautobot/extras/management/commands/runjob_with_job_result.py +46 -0
  205. nautobot/extras/management/utils.py +87 -0
  206. nautobot/extras/managers.py +1 -3
  207. nautobot/extras/migrations/0018_joblog_data_migration.py +9 -7
  208. nautobot/extras/migrations/0117_create_job_queue_model.py +129 -0
  209. nautobot/extras/migrations/0118_task_queue_to_job_queue_migration.py +78 -0
  210. nautobot/extras/migrations/0119_remove_task_queues_from_job_and_queue_from_scheduled_job.py +28 -0
  211. nautobot/extras/models/__init__.py +4 -0
  212. nautobot/extras/models/change_logging.py +7 -3
  213. nautobot/extras/models/customfields.py +11 -12
  214. nautobot/extras/models/groups.py +9 -13
  215. nautobot/extras/models/jobs.py +218 -37
  216. nautobot/extras/models/models.py +2 -2
  217. nautobot/extras/models/relationships.py +69 -1
  218. nautobot/extras/models/secrets.py +5 -0
  219. nautobot/extras/navigation.py +20 -262
  220. nautobot/extras/plugins/__init__.py +56 -32
  221. nautobot/extras/plugins/marketplace_manifest.yml +450 -0
  222. nautobot/extras/plugins/urls.py +1 -0
  223. nautobot/extras/plugins/views.py +48 -1
  224. nautobot/extras/signals.py +39 -1
  225. nautobot/extras/tables.py +40 -6
  226. nautobot/extras/templates/extras/externalintegration_retrieve.html +0 -47
  227. nautobot/extras/templates/extras/inc/tags_panel.html +1 -5
  228. nautobot/extras/templates/extras/job_bulk_edit.html +2 -1
  229. nautobot/extras/templates/extras/job_detail.html +36 -6
  230. nautobot/extras/templates/extras/job_edit.html +5 -2
  231. nautobot/extras/templates/extras/job_list.html +2 -7
  232. nautobot/extras/templates/extras/jobqueue_retrieve.html +44 -0
  233. nautobot/extras/templates/extras/marketplace.html +278 -0
  234. nautobot/extras/templates/extras/plugins_list.html +35 -1
  235. nautobot/extras/templates/extras/plugins_tiles.html +79 -0
  236. nautobot/extras/templates/extras/role_retrieve.html +16 -0
  237. nautobot/extras/templates/extras/secret.html +0 -65
  238. nautobot/extras/templates/extras/secret_check.js +16 -0
  239. nautobot/extras/templates/extras/secret_create.html +114 -0
  240. nautobot/extras/templates/extras/secret_edit.html +1 -114
  241. nautobot/extras/templates/extras/secretsgroup_edit.html +1 -1
  242. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -0
  243. nautobot/extras/templatetags/job_buttons.py +5 -4
  244. nautobot/extras/templatetags/plugins.py +69 -6
  245. nautobot/extras/test_jobs/api_test_job.py +1 -1
  246. nautobot/extras/test_jobs/atomic_transaction.py +2 -2
  247. nautobot/extras/test_jobs/dry_run.py +1 -1
  248. nautobot/extras/test_jobs/fail.py +5 -5
  249. nautobot/extras/test_jobs/file_output.py +1 -1
  250. nautobot/extras/test_jobs/file_upload_fail.py +1 -1
  251. nautobot/extras/test_jobs/file_upload_pass.py +1 -1
  252. nautobot/extras/test_jobs/ipaddress_vars.py +1 -3
  253. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +1 -1
  254. nautobot/extras/test_jobs/location_with_custom_field.py +1 -1
  255. nautobot/extras/test_jobs/log_redaction.py +1 -1
  256. nautobot/extras/test_jobs/log_skip_db_logging.py +1 -1
  257. nautobot/extras/test_jobs/modify_db.py +1 -1
  258. nautobot/extras/test_jobs/object_var_optional.py +1 -1
  259. nautobot/extras/test_jobs/object_var_required.py +1 -1
  260. nautobot/extras/test_jobs/object_vars.py +1 -1
  261. nautobot/extras/test_jobs/pass.py +3 -3
  262. nautobot/extras/test_jobs/profiling.py +1 -1
  263. nautobot/extras/test_jobs/relative_import.py +3 -3
  264. nautobot/extras/test_jobs/soft_time_limit_greater_than_time_limit.py +1 -1
  265. nautobot/extras/test_jobs/task_queues.py +1 -1
  266. nautobot/extras/tests/integration/test_plugin_banner.py +2 -0
  267. nautobot/extras/tests/test_api.py +157 -55
  268. nautobot/extras/tests/test_context_managers.py +4 -1
  269. nautobot/extras/tests/test_customfields.py +1 -1
  270. nautobot/extras/tests/test_datasources.py +1 -2
  271. nautobot/extras/tests/test_dynamicgroups.py +1 -1
  272. nautobot/extras/tests/test_filters.py +219 -535
  273. nautobot/extras/tests/test_forms.py +1 -20
  274. nautobot/extras/tests/test_job_variables.py +73 -152
  275. nautobot/extras/tests/test_jobs.py +43 -54
  276. nautobot/extras/tests/test_models.py +71 -16
  277. nautobot/extras/tests/test_relationships.py +5 -2
  278. nautobot/extras/tests/test_utils.py +23 -2
  279. nautobot/extras/tests/test_views.py +183 -43
  280. nautobot/extras/tests/test_webhooks.py +2 -1
  281. nautobot/extras/urls.py +2 -20
  282. nautobot/extras/utils.py +118 -4
  283. nautobot/extras/views.py +203 -92
  284. nautobot/extras/webhooks.py +5 -2
  285. nautobot/ipam/api/fields.py +3 -3
  286. nautobot/ipam/api/serializers.py +36 -137
  287. nautobot/ipam/api/views.py +93 -62
  288. nautobot/ipam/lookups.py +62 -101
  289. nautobot/ipam/models.py +11 -63
  290. nautobot/ipam/navigation.py +0 -90
  291. nautobot/ipam/querysets.py +2 -2
  292. nautobot/ipam/tables.py +6 -20
  293. nautobot/ipam/templates/ipam/routetarget.html +0 -28
  294. nautobot/ipam/templates/ipam/vrf.html +0 -47
  295. nautobot/ipam/tests/test_api.py +8 -419
  296. nautobot/ipam/tests/test_filters.py +39 -119
  297. nautobot/ipam/tests/test_forms.py +47 -51
  298. nautobot/ipam/tests/test_migrations.py +30 -30
  299. nautobot/ipam/tests/test_models.py +0 -41
  300. nautobot/ipam/tests/test_querysets.py +1 -63
  301. nautobot/ipam/urls.py +3 -69
  302. nautobot/ipam/utils/__init__.py +0 -24
  303. nautobot/ipam/views.py +153 -198
  304. nautobot/project-static/css/base.css +38 -3
  305. nautobot/project-static/docs/404.html +421 -19
  306. nautobot/project-static/docs/apps/index.html +421 -19
  307. nautobot/project-static/docs/apps/nautobot-apps.html +421 -19
  308. nautobot/project-static/docs/assets/extra.css +5 -1
  309. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js → bundle.83f73b43.min.js} +2 -2
  310. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js.map → bundle.83f73b43.min.js.map} +2 -2
  311. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +421 -19
  312. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +421 -19
  313. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +421 -172
  314. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +421 -19
  315. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +421 -19
  316. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +425 -21
  317. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +421 -19
  318. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +424 -22
  319. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +9809 -0
  320. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +424 -63
  321. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +421 -19
  322. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +421 -19
  323. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +457 -20
  324. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +421 -19
  325. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +425 -25
  326. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +457 -19
  327. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +421 -19
  328. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +421 -19
  329. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +425 -215
  330. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +430 -342
  331. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +5799 -1054
  332. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +421 -19
  333. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +447 -176
  334. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +460 -21
  335. nautobot/project-static/docs/development/apps/api/configuration-view.html +421 -19
  336. nautobot/project-static/docs/development/apps/api/database-backend-config.html +421 -19
  337. nautobot/project-static/docs/development/apps/api/models/django-admin.html +421 -19
  338. nautobot/project-static/docs/development/apps/api/models/global-search.html +421 -19
  339. nautobot/project-static/docs/development/apps/api/models/graphql.html +421 -19
  340. nautobot/project-static/docs/development/apps/api/models/index.html +421 -19
  341. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +421 -19
  342. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +421 -19
  343. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +421 -19
  344. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +421 -19
  345. nautobot/project-static/docs/development/apps/api/platform-features/index.html +421 -19
  346. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +421 -19
  347. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +421 -19
  348. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +421 -19
  349. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +421 -19
  350. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +424 -41
  351. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +421 -19
  352. nautobot/project-static/docs/development/apps/api/prometheus.html +421 -19
  353. nautobot/project-static/docs/development/apps/api/setup.html +425 -155
  354. nautobot/project-static/docs/development/apps/api/testing.html +421 -19
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +421 -19
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +421 -19
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +421 -19
  358. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +421 -19
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +701 -130
  360. nautobot/project-static/docs/development/apps/api/views/base-template.html +421 -19
  361. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +421 -19
  362. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +421 -19
  363. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +421 -19
  364. nautobot/project-static/docs/development/apps/api/views/index.html +423 -20
  365. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +425 -19
  366. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +451 -19
  367. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +421 -19
  368. nautobot/project-static/docs/development/apps/api/views/notes.html +421 -19
  369. nautobot/project-static/docs/development/apps/api/views/rest-api.html +421 -19
  370. nautobot/project-static/docs/development/apps/api/views/urls.html +421 -19
  371. nautobot/project-static/docs/development/apps/index.html +421 -19
  372. nautobot/project-static/docs/development/apps/migration/code-updates.html +422 -52
  373. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +422 -20
  374. nautobot/project-static/docs/development/apps/migration/from-v1.html +421 -19
  375. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +421 -19
  376. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +421 -19
  377. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +421 -19
  378. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +424 -22
  379. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +9219 -0
  380. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +9333 -0
  381. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +9474 -0
  382. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +9517 -0
  383. nautobot/project-static/docs/development/apps/porting-from-netbox.html +424 -22
  384. nautobot/project-static/docs/development/core/application-registry.html +421 -19
  385. nautobot/project-static/docs/development/core/best-practices.html +421 -19
  386. nautobot/project-static/docs/development/core/bootstrap-ui.html +421 -19
  387. nautobot/project-static/docs/development/core/caching.html +421 -19
  388. nautobot/project-static/docs/development/core/controllers.html +423 -19
  389. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +490 -45
  390. nautobot/project-static/docs/development/core/generic-views.html +421 -19
  391. nautobot/project-static/docs/development/core/getting-started.html +566 -179
  392. nautobot/project-static/docs/development/core/homepage.html +432 -30
  393. nautobot/project-static/docs/development/core/index.html +421 -19
  394. nautobot/project-static/docs/development/core/local-k8s.html +9453 -0
  395. nautobot/project-static/docs/development/core/model-checklist.html +424 -22
  396. nautobot/project-static/docs/development/core/model-features.html +421 -19
  397. nautobot/project-static/docs/development/core/natural-keys.html +421 -19
  398. nautobot/project-static/docs/development/core/navigation-menu.html +438 -26
  399. nautobot/project-static/docs/development/core/release-checklist.html +435 -45
  400. nautobot/project-static/docs/development/core/role-internals.html +421 -19
  401. nautobot/project-static/docs/development/core/settings.html +421 -19
  402. nautobot/project-static/docs/development/core/style-guide.html +421 -19
  403. nautobot/project-static/docs/development/core/templates.html +431 -22
  404. nautobot/project-static/docs/development/core/testing.html +421 -19
  405. nautobot/project-static/docs/development/core/ui-component-framework.html +11020 -0
  406. nautobot/project-static/docs/development/core/user-preferences.html +424 -22
  407. nautobot/project-static/docs/development/index.html +421 -19
  408. nautobot/project-static/docs/development/jobs/index.html +546 -160
  409. nautobot/project-static/docs/development/jobs/migration/from-v1.html +421 -19
  410. nautobot/project-static/docs/index.html +421 -19
  411. nautobot/project-static/docs/media/development/core/ui-component-framework/basic-panel-layout.png +0 -0
  412. nautobot/project-static/docs/media/development/core/ui-component-framework/button-example.png +0 -0
  413. nautobot/project-static/docs/media/development/core/ui-component-framework/dropdown-button-example.png +0 -0
  414. nautobot/project-static/docs/media/development/core/ui-component-framework/grouped-key-value-table-panel-example-1.png +0 -0
  415. nautobot/project-static/docs/media/development/core/ui-component-framework/grouped-key-value-table-panel-example-2.png +0 -0
  416. nautobot/project-static/docs/media/development/core/ui-component-framework/object-fields-panel-example.png +0 -0
  417. nautobot/project-static/docs/media/development/core/ui-component-framework/stats-panel-example.png +0 -0
  418. nautobot/project-static/docs/media/development/core/ui-component-framework/table-panels-family.png +0 -0
  419. nautobot/project-static/docs/media/development/core/ui-component-framework/text-panels-family.png +0 -0
  420. nautobot/project-static/docs/media/development/core/ui-component-framework/ui-framework-example.png +0 -0
  421. nautobot/project-static/docs/media/models/virtual_device_context_overview.drawio +73 -0
  422. nautobot/project-static/docs/media/models/virtual_device_context_overview.png +0 -0
  423. nautobot/project-static/docs/models/dcim/virtualdevicecontext.html +14 -0
  424. nautobot/project-static/docs/models/extras/jobqueue.html +14 -0
  425. nautobot/project-static/docs/models/wireless/radioprofile.html +14 -0
  426. nautobot/project-static/docs/models/wireless/supporteddatarate.html +14 -0
  427. nautobot/project-static/docs/models/wireless/wirelessnetwork.html +14 -0
  428. nautobot/project-static/docs/objects.inv +0 -0
  429. nautobot/project-static/docs/overview/application_stack.html +426 -20
  430. nautobot/project-static/docs/overview/design_philosophy.html +421 -19
  431. nautobot/project-static/docs/release-notes/index.html +445 -22
  432. nautobot/project-static/docs/release-notes/version-1.0.html +421 -19
  433. nautobot/project-static/docs/release-notes/version-1.1.html +421 -19
  434. nautobot/project-static/docs/release-notes/version-1.2.html +421 -19
  435. nautobot/project-static/docs/release-notes/version-1.3.html +421 -19
  436. nautobot/project-static/docs/release-notes/version-1.4.html +421 -19
  437. nautobot/project-static/docs/release-notes/version-1.5.html +421 -19
  438. nautobot/project-static/docs/release-notes/version-1.6.html +634 -667
  439. nautobot/project-static/docs/release-notes/version-2.0.html +421 -19
  440. nautobot/project-static/docs/release-notes/version-2.1.html +421 -19
  441. nautobot/project-static/docs/release-notes/version-2.2.html +421 -19
  442. nautobot/project-static/docs/release-notes/version-2.3.html +684 -886
  443. nautobot/project-static/docs/release-notes/version-2.4.html +10007 -0
  444. nautobot/project-static/docs/requirements.txt +2 -2
  445. nautobot/project-static/docs/search/search_index.json +1 -1
  446. nautobot/project-static/docs/sitemap.xml +334 -270
  447. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  448. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +421 -19
  449. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +421 -19
  450. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +423 -21
  451. nautobot/project-static/docs/user-guide/administration/configuration/index.html +433 -32
  452. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +421 -19
  453. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +765 -180
  454. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +421 -19
  455. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +421 -19
  456. nautobot/project-static/docs/user-guide/administration/guides/docker.html +434 -29
  457. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +421 -19
  458. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +421 -19
  459. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +421 -19
  460. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +421 -19
  461. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +421 -19
  462. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +421 -19
  463. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +421 -19
  464. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +421 -19
  465. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +421 -19
  466. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +421 -19
  467. nautobot/project-static/docs/user-guide/administration/installation/index.html +426 -20
  468. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +421 -19
  469. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +421 -19
  470. nautobot/project-static/docs/user-guide/administration/installation/services.html +421 -19
  471. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +421 -19
  472. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +442 -41
  473. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +435 -66
  474. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +435 -66
  475. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +421 -19
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +421 -19
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +421 -19
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +421 -19
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +421 -19
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +421 -19
  481. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +421 -19
  482. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-nautobot-app-location.yaml +0 -16
  483. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +421 -19
  484. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +427 -21
  485. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +421 -19
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +421 -19
  487. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +421 -19
  488. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +421 -19
  489. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +421 -19
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +421 -19
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +421 -19
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +421 -19
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +421 -19
  494. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +421 -19
  495. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +421 -19
  496. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +421 -19
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +421 -19
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +421 -19
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +421 -19
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +421 -19
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +421 -19
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +457 -20
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +447 -22
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +421 -19
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +421 -19
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +421 -19
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +421 -19
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +421 -19
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +421 -19
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +421 -19
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +421 -19
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +421 -19
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +421 -19
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +421 -19
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +421 -19
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +421 -19
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +421 -19
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +421 -19
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +421 -19
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +421 -19
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +421 -19
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +421 -19
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +421 -19
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +421 -19
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +421 -19
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +421 -19
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +421 -19
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +421 -19
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +421 -19
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +421 -19
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +421 -19
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +421 -19
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +421 -19
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +421 -19
  535. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +421 -19
  536. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +421 -19
  537. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +421 -19
  538. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +9333 -0
  539. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +424 -22
  540. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +421 -19
  541. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +421 -19
  542. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +421 -19
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +421 -19
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +421 -19
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +421 -19
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +421 -19
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +421 -19
  548. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +421 -19
  549. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +421 -19
  550. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +421 -19
  551. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +421 -19
  552. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +421 -19
  553. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +421 -19
  554. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +421 -19
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +421 -19
  556. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +421 -19
  557. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +421 -19
  558. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +421 -19
  559. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +424 -22
  560. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +9271 -0
  561. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +9175 -0
  562. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +9169 -0
  563. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +9235 -0
  564. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +421 -19
  565. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +421 -19
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +421 -19
  567. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +421 -19
  568. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +421 -19
  569. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +421 -19
  570. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +421 -19
  571. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +421 -19
  572. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +421 -19
  573. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +421 -19
  574. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +421 -19
  575. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +421 -19
  576. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +421 -19
  577. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +421 -19
  578. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +421 -19
  579. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +421 -19
  580. nautobot/project-static/docs/user-guide/index.html +421 -19
  581. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +424 -22
  582. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +421 -19
  583. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +421 -19
  584. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +421 -19
  585. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +421 -19
  586. nautobot/project-static/docs/user-guide/platform-functionality/events.html +9575 -0
  587. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +424 -22
  588. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +421 -19
  589. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +421 -19
  590. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +421 -19
  591. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +421 -19
  592. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +421 -19
  593. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +426 -20
  594. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +424 -22
  595. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +421 -19
  596. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +421 -19
  597. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +9182 -0
  598. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +424 -22
  599. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +421 -19
  600. nautobot/project-static/docs/user-guide/platform-functionality/note.html +421 -19
  601. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +421 -19
  602. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +424 -22
  603. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +9250 -0
  604. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +421 -19
  605. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +421 -19
  606. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +424 -22
  607. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +421 -19
  608. nautobot/project-static/docs/user-guide/platform-functionality/role.html +421 -19
  609. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +421 -19
  610. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +421 -19
  611. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +424 -22
  612. nautobot/project-static/docs/user-guide/platform-functionality/status.html +421 -19
  613. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +421 -19
  614. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +489 -56
  615. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +421 -19
  616. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +421 -19
  617. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +421 -19
  618. nautobot/project-static/img/jinja_logo.svg +97 -0
  619. nautobot/project-static/js/forms.js +5 -0
  620. nautobot/project-static/js/nav_menu.js +2 -1
  621. nautobot/tenancy/api/serializers.py +0 -2
  622. nautobot/tenancy/factory.py +1 -1
  623. nautobot/tenancy/navigation.py +0 -29
  624. nautobot/tenancy/templates/tenancy/tenant.html +4 -91
  625. nautobot/tenancy/tests/test_filters.py +29 -134
  626. nautobot/tenancy/views.py +32 -23
  627. nautobot/users/admin.py +3 -1
  628. nautobot/users/api/serializers.py +4 -5
  629. nautobot/users/api/views.py +1 -1
  630. nautobot/users/forms.py +19 -0
  631. nautobot/users/templates/users/preferences.html +22 -0
  632. nautobot/users/tests/test_filters.py +1 -19
  633. nautobot/users/tests/test_views.py +57 -0
  634. nautobot/users/utils.py +8 -0
  635. nautobot/users/views.py +48 -11
  636. nautobot/virtualization/api/serializers.py +4 -4
  637. nautobot/virtualization/filters.py +2 -20
  638. nautobot/virtualization/navigation.py +0 -48
  639. nautobot/virtualization/templates/virtualization/clustertype.html +0 -39
  640. nautobot/virtualization/tests/test_filters.py +57 -183
  641. nautobot/virtualization/views.py +18 -15
  642. nautobot/wireless/__init__.py +0 -0
  643. nautobot/wireless/api/__init__.py +0 -0
  644. nautobot/wireless/api/serializers.py +44 -0
  645. nautobot/wireless/api/urls.py +20 -0
  646. nautobot/wireless/api/views.py +34 -0
  647. nautobot/wireless/apps.py +8 -0
  648. nautobot/wireless/choices.py +345 -0
  649. nautobot/wireless/factory.py +138 -0
  650. nautobot/wireless/filters.py +167 -0
  651. nautobot/wireless/forms.py +283 -0
  652. nautobot/wireless/homepage.py +19 -0
  653. nautobot/wireless/migrations/0001_initial.py +223 -0
  654. nautobot/wireless/migrations/__init__.py +0 -0
  655. nautobot/wireless/models.py +207 -0
  656. nautobot/wireless/navigation.py +105 -0
  657. nautobot/wireless/tables.py +244 -0
  658. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +81 -0
  659. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +26 -0
  660. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +88 -0
  661. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +64 -0
  662. nautobot/wireless/tests/__init__.py +0 -0
  663. nautobot/wireless/tests/test_api.py +247 -0
  664. nautobot/wireless/tests/test_filters.py +54 -0
  665. nautobot/wireless/tests/test_models.py +22 -0
  666. nautobot/wireless/tests/test_views.py +378 -0
  667. nautobot/wireless/urls.py +13 -0
  668. nautobot/wireless/views.py +129 -0
  669. {nautobot-2.3.16.dist-info → nautobot-2.4.0b1.dist-info}/METADATA +11 -14
  670. {nautobot-2.3.16.dist-info → nautobot-2.4.0b1.dist-info}/RECORD +674 -551
  671. {nautobot-2.3.16.dist-info → nautobot-2.4.0b1.dist-info}/WHEEL +1 -1
  672. nautobot/core/utils/navigation.py +0 -54
  673. {nautobot-2.3.16.dist-info → nautobot-2.4.0b1.dist-info}/LICENSE.txt +0 -0
  674. {nautobot-2.3.16.dist-info → nautobot-2.4.0b1.dist-info}/NOTICE +0 -0
  675. {nautobot-2.3.16.dist-info → nautobot-2.4.0b1.dist-info}/entry_points.txt +0 -0
@@ -1,19 +1,13 @@
1
1
  from datetime import datetime
2
2
  import uuid
3
+ from zoneinfo import ZoneInfo
3
4
 
4
5
  from django.contrib.auth import get_user_model
5
6
  from django.contrib.contenttypes.models import ContentType
6
7
  from django.core.files.uploadedfile import SimpleUploadedFile
7
- from django.db.models import Q
8
8
  from django.test import override_settings, RequestFactory
9
9
  from django.utils.timezone import now
10
10
 
11
- try:
12
- from zoneinfo import ZoneInfo
13
- except ImportError: # Python 3.8
14
- from backports.zoneinfo import ZoneInfo
15
-
16
- from nautobot.core.choices import ColorChoices
17
11
  from nautobot.core.testing import FilterTestCases
18
12
  from nautobot.dcim.filters import DeviceFilterSet
19
13
  from nautobot.dcim.models import (
@@ -30,6 +24,7 @@ from nautobot.extras.choices import (
30
24
  CustomFieldTypeChoices,
31
25
  DynamicGroupTypeChoices,
32
26
  JobExecutionType,
27
+ JobQueueTypeChoices,
33
28
  JobResultStatusChoices,
34
29
  MetadataTypeDataTypeChoices,
35
30
  ObjectChangeActionChoices,
@@ -55,6 +50,8 @@ from nautobot.extras.filters import (
55
50
  JobFilterSet,
56
51
  JobHookFilterSet,
57
52
  JobLogEntryFilterSet,
53
+ JobQueueAssignmentFilterSet,
54
+ JobQueueFilterSet,
58
55
  JobResultFilterSet,
59
56
  MetadataChoiceFilterSet,
60
57
  MetadataTypeFilterSet,
@@ -92,6 +89,8 @@ from nautobot.extras.models import (
92
89
  JobButton,
93
90
  JobHook,
94
91
  JobLogEntry,
92
+ JobQueue,
93
+ JobQueueAssignment,
95
94
  JobResult,
96
95
  MetadataChoice,
97
96
  MetadataType,
@@ -116,7 +115,7 @@ from nautobot.ipam.filters import VLANFilterSet
116
115
  from nautobot.ipam.models import IPAddress, Namespace, Prefix, VLAN, VLANGroup
117
116
  from nautobot.tenancy.models import Tenant, TenantGroup
118
117
  from nautobot.users.factory import UserFactory
119
- from nautobot.virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
118
+ from nautobot.virtualization.models import Cluster, ClusterGroup, ClusterType
120
119
 
121
120
  # Use the proper swappable User model
122
121
  User = get_user_model()
@@ -125,6 +124,12 @@ User = get_user_model()
125
124
  class ComputedFieldTestCase(FilterTestCases.FilterTestCase):
126
125
  queryset = ComputedField.objects.all()
127
126
  filterset = ComputedFieldFilterSet
127
+ generic_filter_tests = (
128
+ ("fallback_value",),
129
+ ("key",),
130
+ ("template",),
131
+ ("weight",),
132
+ )
128
133
 
129
134
  @classmethod
130
135
  def setUpTestData(cls):
@@ -160,50 +165,38 @@ class ComputedFieldTestCase(FilterTestCases.FilterTestCase):
160
165
  label="Device Computed Field",
161
166
  template="Hello, world.",
162
167
  fallback_value="This template has errored",
163
- weight=100,
168
+ weight=300,
164
169
  )
165
170
 
166
- def test_key(self):
167
- params = {"key": ["device_computed_field", "worse_computed_field"]}
168
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
169
-
170
171
  def test_content_type(self):
171
172
  params = {"content_type": "dcim.location"}
172
173
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
173
174
 
174
- def test_template(self):
175
- params = {"template": ["Hello, world."]}
176
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
177
-
178
- def test_fallback_value(self):
179
- params = {"fallback_value": ["This template has errored"]}
180
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
181
-
182
- def test_weight(self):
183
- params = {"weight": [100]}
184
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
185
-
186
- def test_search(self):
187
- # label
188
- params = {"q": "Field One"}
189
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
190
- # content_type__app_label
191
- params = {"q": "dcim"}
192
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
193
- # content_type__model
194
- params = {"q": "location"}
195
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
196
- # template
197
- params = {"q": "hello"}
198
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
199
- # fallback_value
200
- params = {"q": "has errored"}
201
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
202
-
203
175
 
204
176
  class ConfigContextTestCase(FilterTestCases.FilterTestCase):
205
177
  queryset = ConfigContext.objects.all()
206
178
  filterset = ConfigContextFilterSet
179
+ generic_filter_tests = (
180
+ ("cluster_id", "clusters__id"),
181
+ ("cluster_group", "cluster_groups__id"),
182
+ ("cluster_group", "cluster_groups__name"),
183
+ ("cluster_group_id", "cluster_groups__id"),
184
+ ("device_type", "device_types__id"),
185
+ ("device_type", "device_types__model"),
186
+ ("device_type_id", "device_types__id"),
187
+ ("name",),
188
+ ("platform", "platforms__id"),
189
+ ("platform", "platforms__name"),
190
+ ("platform_id", "platforms__id"),
191
+ ("role", "roles__id"),
192
+ ("role", "roles__name"),
193
+ ("tenant", "tenants__id"),
194
+ ("tenant", "tenants__name"),
195
+ ("tenant_id", "tenants__id"),
196
+ ("tenant_group", "tenant_groups__id"),
197
+ ("tenant_group", "tenant_groups__name"),
198
+ ("tenant_group_id", "tenant_groups__id"),
199
+ )
207
200
 
208
201
  @classmethod
209
202
  def setUpTestData(cls):
@@ -260,10 +253,6 @@ class ConfigContextTestCase(FilterTestCases.FilterTestCase):
260
253
  c.tenants.set([cls.tenants[i]])
261
254
  c.locations.set([cls.locations[i]])
262
255
 
263
- def test_name(self):
264
- params = {"name": ["Config Context 1", "Config Context 2"]}
265
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
266
-
267
256
  def test_is_active(self):
268
257
  params = {"is_active": True}
269
258
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -280,82 +269,6 @@ class ConfigContextTestCase(FilterTestCases.FilterTestCase):
280
269
  self.filterset(params, self.queryset).qs, self.queryset.filter(locations__name__in=params["location"])
281
270
  )
282
271
 
283
- def test_role(self):
284
- device_role = Role.objects.get_for_model(Device).first()
285
- vm_role = Role.objects.get_for_model(VirtualMachine).first()
286
- params = {"role": [device_role.pk, vm_role.name]}
287
- self.assertQuerysetEqualAndNotEmpty(
288
- self.filterset(params, self.queryset).qs,
289
- self.queryset.filter(roles__in=[vm_role, device_role]).distinct(),
290
- )
291
-
292
- def test_type(self):
293
- device_types = list(self.device_types[:2])
294
- filter_params = [
295
- {"device_type_id": [device_types[0].pk, device_types[1].pk]},
296
- {"device_type": [device_types[0].pk, device_types[1].model]},
297
- ]
298
- for params in filter_params:
299
- self.assertQuerysetEqualAndNotEmpty(
300
- self.filterset(params, self.queryset).qs, self.queryset.filter(device_types__in=device_types).distinct()
301
- )
302
-
303
- def test_platform(self):
304
- platforms = list(self.platforms[:2])
305
- filter_params = [
306
- {"platform_id": [platforms[0].pk, platforms[1].pk]},
307
- {"platform": [platforms[0].pk, platforms[1].name]},
308
- ]
309
- for params in filter_params:
310
- self.assertQuerysetEqualAndNotEmpty(
311
- self.filterset(params, self.queryset).qs, self.queryset.filter(platforms__in=platforms).distinct()
312
- )
313
-
314
- def test_cluster_group(self):
315
- cluster_groups = list(ClusterGroup.objects.all()[:2])
316
- filter_params = [
317
- {"cluster_group_id": [cluster_groups[0].pk, cluster_groups[1].pk]},
318
- {"cluster_group": [cluster_groups[0].pk, cluster_groups[1].name]},
319
- ]
320
- for params in filter_params:
321
- self.assertQuerysetEqualAndNotEmpty(
322
- self.filterset(params, self.queryset).qs,
323
- self.queryset.filter(cluster_groups__in=cluster_groups).distinct(),
324
- )
325
-
326
- def test_cluster(self):
327
- clusters = Cluster.objects.all()[:2]
328
- params = {"cluster_id": [clusters[0].pk, clusters[1].pk]}
329
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
330
-
331
- def test_tenant_group(self):
332
- tenant_groups = list(self.tenant_groups[:2])
333
- filter_params = [
334
- {"tenant_group_id": [tenant_groups[0].pk, tenant_groups[1].pk]},
335
- {"tenant_group": [tenant_groups[0].name, tenant_groups[1].pk]},
336
- ]
337
- for params in filter_params:
338
- self.assertQuerysetEqualAndNotEmpty(
339
- self.filterset(params, self.queryset).qs,
340
- self.queryset.filter(tenant_groups__in=tenant_groups).distinct(),
341
- )
342
-
343
- def test_tenant(self):
344
- tenants = list(self.tenants[:2])
345
- filter_params = [
346
- {"tenant_id": [tenants[0].pk, tenants[1].pk]},
347
- {"tenant": [tenants[0].name, tenants[1].pk]},
348
- ]
349
- for params in filter_params:
350
- self.assertQuerysetEqualAndNotEmpty(
351
- self.filterset(params, self.queryset).qs, self.queryset.filter(tenants__in=tenants).distinct()
352
- )
353
-
354
- def test_search(self):
355
- value = self.queryset.values_list("pk", flat=True)[0]
356
- params = {"q": value}
357
- self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
358
-
359
272
  @override_settings(CONFIG_CONTEXT_DYNAMIC_GROUPS_ENABLED=True)
360
273
  def test_with_dynamic_groups_enabled(self):
361
274
  """Asserts that `ConfigContextFilterSet.dynamic_group` is present when feature flag is enabled."""
@@ -372,6 +285,10 @@ class ConfigContextTestCase(FilterTestCases.FilterTestCase):
372
285
  class ContentTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
373
286
  queryset = ContentType.objects.order_by("app_label", "model")
374
287
  filterset = ContentTypeFilterSet
288
+ generic_filter_tests = (
289
+ ("app_label",),
290
+ ("model",),
291
+ )
375
292
  user_permissions = [
376
293
  "dcim.add_location",
377
294
  "extras.change_status",
@@ -383,16 +300,6 @@ class ContentTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
383
300
  super().setUp()
384
301
  self.factory = RequestFactory(SERVER_NAME="nautobot.example.com")
385
302
 
386
- def test_app_label(self):
387
- params = {"app_label": ["dcim"]}
388
- self.assertQuerysetEqual(self.filterset(params, self.queryset).qs, self.queryset.filter(app_label="dcim"))
389
-
390
- def test_model(self):
391
- params = {"model": ["device", "virtualmachine"]}
392
- self.assertQuerysetEqual(
393
- self.filterset(params, self.queryset).qs, self.queryset.filter(model__in=["device", "virtualmachine"])
394
- )
395
-
396
303
  def test_can_add(self):
397
304
  # With no request user, can't add anything
398
305
  params = {"can_add": True}
@@ -473,13 +380,6 @@ class ContentTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
473
380
  self.queryset.exclude(app_label="tenancy", model="tenant"),
474
381
  )
475
382
 
476
- def test_search(self):
477
- params = {"q": "circ"}
478
- self.assertQuerysetEqual(
479
- self.filterset(params, self.queryset).qs,
480
- self.queryset.filter(Q(app_label__icontains="circ") | Q(model__icontains="circ")),
481
- )
482
-
483
383
 
484
384
  class ContactAndTeamFilterSetTestCaseMixin:
485
385
  """Mixin class to test common filters to both Contact and Team filter sets."""
@@ -647,33 +547,17 @@ class ContactAssociationFilterSetTestCase(FilterTestCases.FilterTestCase):
647
547
  )
648
548
 
649
549
 
650
- class CustomFieldChoiceFilterSetTestCase(FilterTestCases.FilterTestCase):
651
- queryset = CustomFieldChoice.objects.all()
652
- filterset = CustomFieldChoiceFilterSet
653
-
654
- generic_filter_tests = (
655
- ["value"],
656
- ["custom_field", "custom_field__key"],
657
- ["weight"],
658
- )
659
-
660
- @classmethod
661
- def setUpTestData(cls):
662
- obj_type = ContentType.objects.get_for_model(Device)
663
- cfs = [
664
- CustomField.objects.create(label=f"Custom Field {num}", type=CustomFieldTypeChoices.TYPE_TEXT)
665
- for num in range(3)
666
- ]
667
- for cf in cfs:
668
- cf.content_types.set([obj_type])
669
-
670
- for i, val in enumerate(["Value 1", "Value 2", "Value 3"]):
671
- CustomFieldChoice.objects.create(custom_field=cfs[i], value=val, weight=100 * i)
672
-
673
-
674
550
  class CustomLinkTestCase(FilterTestCases.FilterTestCase):
675
551
  queryset = CustomLink.objects.all()
676
552
  filterset = CustomLinkFilterSet
553
+ generic_filter_tests = (
554
+ # ("button_class",), # TODO
555
+ # ("group_name",), # TODO
556
+ ("name",),
557
+ ("target_url",),
558
+ ("text",),
559
+ ("weight",),
560
+ )
677
561
 
678
562
  @classmethod
679
563
  def setUpTestData(cls):
@@ -693,7 +577,7 @@ class CustomLinkTestCase(FilterTestCases.FilterTestCase):
693
577
  name="customlink-2",
694
578
  text="customlink text 2",
695
579
  target_url="http://customlink2.com",
696
- weight=100,
580
+ weight=200,
697
581
  button_class="default",
698
582
  new_window=False,
699
583
  )
@@ -702,31 +586,21 @@ class CustomLinkTestCase(FilterTestCases.FilterTestCase):
702
586
  name="customlink-3",
703
587
  text="customlink text 3",
704
588
  target_url="http://customlink3.com",
705
- weight=100,
589
+ weight=300,
706
590
  button_class="default",
707
591
  new_window=False,
708
592
  )
709
593
 
710
- def test_name(self):
711
- params = {"name": ["customlink-1"]}
712
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
713
-
714
- def test_target_url(self):
715
- params = {"target_url": ["http://customlink1.com"]}
716
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
717
-
718
- def test_weight(self):
719
- params = {"weight": [100]}
720
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
721
-
722
- def test_search(self):
723
- params = {"q": "customlink"}
724
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
725
-
726
594
 
727
595
  class CustomFieldChoiceTestCase(FilterTestCases.FilterTestCase):
728
596
  queryset = CustomFieldChoice.objects.all()
729
597
  filterset = CustomFieldChoiceFilterSet
598
+ generic_filter_tests = (
599
+ ("custom_field", "custom_field__key"),
600
+ ("custom_field", "custom_field__id"),
601
+ ("value",),
602
+ ("weight",),
603
+ )
730
604
 
731
605
  @classmethod
732
606
  def setUpTestData(cls):
@@ -739,23 +613,14 @@ class CustomFieldChoiceTestCase(FilterTestCases.FilterTestCase):
739
613
  for field in fields:
740
614
  field.content_types.set([content_type])
741
615
 
742
- for num in range(3):
743
- CustomFieldChoice.objects.create(custom_field=fields[num], value=f"Custom Field Choice {num}")
744
-
745
- def test_field(self):
746
- fields = list(self.fields[:2])
747
- filter_params = [
748
- {"custom_field": [fields[0].key, fields[1].pk]},
749
- ]
750
- for params in filter_params:
751
- self.assertQuerysetEqualAndNotEmpty(
752
- self.filterset(params, self.queryset).qs, self.queryset.filter(custom_field__in=fields).distinct()
753
- )
616
+ for i, val in enumerate(["Value 1", "Value 2", "Value 3"]):
617
+ CustomFieldChoice.objects.create(custom_field=fields[i], value=val, weight=100 * i)
754
618
 
755
619
 
756
620
  class ExportTemplateTestCase(FilterTestCases.FilterTestCase):
757
621
  queryset = ExportTemplate.objects.all()
758
622
  filterset = ExportTemplateFilterSet
623
+ generic_filter_tests = (("name",),)
759
624
 
760
625
  @classmethod
761
626
  def setUpTestData(cls):
@@ -782,18 +647,10 @@ class ExportTemplateTestCase(FilterTestCases.FilterTestCase):
782
647
  owner=repo,
783
648
  )
784
649
 
785
- def test_name(self):
786
- params = {"name": ["Export Template 1", "Export Template 2"]}
787
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
788
-
789
650
  def test_content_type(self):
790
651
  params = {"content_type": ContentType.objects.get(model="location").pk}
791
652
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
792
653
 
793
- def test_search(self):
794
- params = {"q": "export"}
795
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
796
-
797
654
 
798
655
  class FileProxyTestCase(FilterTestCases.FilterTestCase):
799
656
  queryset = FileProxy.objects.all()
@@ -842,34 +699,6 @@ class ExternalIntegrationTestCase(FilterTestCases.FilterTestCase):
842
699
  for ei in external_integrations:
843
700
  ei.validated_save()
844
701
 
845
- def test_search(self):
846
- match_name = self.queryset.values_list("name", flat=True)[0].upper()
847
- params = {"q": match_name}
848
- expected_matches = (
849
- Q(id__iexact=match_name) | Q(name__icontains=match_name) | Q(remote_url__icontains=match_name) # pylint: disable=unsupported-binary-operation
850
- )
851
- self.assertQuerysetEqualAndNotEmpty(
852
- self.filterset(params, self.queryset).qs, self.queryset.filter(expected_matches)
853
- )
854
-
855
- match_remote_url = self.queryset.values_list("remote_url", flat=True)[0].upper()
856
- expected_matches = (
857
- Q(id__iexact=match_remote_url) # pylint: disable=unsupported-binary-operation
858
- | Q(name__icontains=match_remote_url)
859
- | Q(remote_url__icontains=match_remote_url)
860
- )
861
- params = {"q": match_remote_url}
862
- self.assertQuerysetEqualAndNotEmpty(
863
- self.filterset(params, self.queryset).qs, self.queryset.filter(expected_matches)
864
- )
865
-
866
- match_pk = str(self.queryset.values_list("pk", flat=True)[0]).title()
867
- expected_matches = Q(id__iexact=match_pk) | Q(name__icontains=match_pk) | Q(remote_url__icontains=match_pk) # pylint: disable=unsupported-binary-operation
868
- params = {"q": match_pk}
869
- self.assertQuerysetEqualAndNotEmpty(
870
- self.filterset(params, self.queryset).qs, self.queryset.filter(expected_matches)
871
- )
872
-
873
702
  def test_verify_ssl(self):
874
703
  params = {"verify_ssl": True}
875
704
  self.assertQuerysetEqualAndNotEmpty(
@@ -884,6 +713,16 @@ class ExternalIntegrationTestCase(FilterTestCases.FilterTestCase):
884
713
  class GitRepositoryTestCase(FilterTestCases.FilterTestCase):
885
714
  queryset = GitRepository.objects.all()
886
715
  filterset = GitRepositoryFilterSet
716
+ generic_filter_tests = (
717
+ ("branch",),
718
+ ("name",),
719
+ # ("provided_contents",), # TODO
720
+ ("remote_url",),
721
+ ("secrets_group", "secrets_group__id"),
722
+ ("secrets_group", "secrets_group__name"),
723
+ ("secrets_group_id", "secrets_group__id"),
724
+ ("slug",),
725
+ )
887
726
 
888
727
  @classmethod
889
728
  def setUpTestData(cls):
@@ -932,43 +771,17 @@ class GitRepositoryTestCase(FilterTestCases.FilterTestCase):
932
771
  repos[0].tags.set(Tag.objects.get_for_model(GitRepository))
933
772
  repos[1].tags.set(Tag.objects.get_for_model(GitRepository)[:3])
934
773
 
935
- def test_id(self):
936
- params = {"id": self.queryset.values_list("pk", flat=True)[:2]}
937
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
938
-
939
- def test_name(self):
940
- params = {"name": ["Repo 3", "Repo 2"]}
941
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
942
-
943
- def test_remote_url(self):
944
- params = {"remote_url": ["https://example.com/repo1.git"]}
945
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
946
-
947
- def test_branch(self):
948
- params = {"branch": ["main", "next"]}
949
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
950
-
951
774
  def test_provided_contents(self):
952
775
  params = {"provided_contents": ["extras.exporttemplate"]}
953
776
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
954
777
  params = {"provided_contents": ["extras.job"]}
955
778
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
956
779
 
957
- def test_secrets_group(self):
958
- filter_params = [
959
- {"secrets_group_id": [self.secrets_groups[0].pk, self.secrets_groups[1].pk]},
960
- {"secrets_group": [self.secrets_groups[0].name, self.secrets_groups[1].pk]},
961
- ]
962
- for params in filter_params:
963
- self.assertQuerysetEqualAndNotEmpty(
964
- self.filterset(params, self.queryset).qs,
965
- self.queryset.filter(secrets_group__in=self.secrets_groups).distinct(),
966
- )
967
-
968
780
 
969
- class GraphQLTestCase(FilterTestCases.NameOnlyFilterTestCase):
781
+ class GraphQLTestCase(FilterTestCases.FilterTestCase):
970
782
  queryset = GraphQLQuery.objects.all()
971
783
  filterset = GraphQLQueryFilterSet
784
+ generic_filter_tests = (("name",),)
972
785
  # skip testing "query" attribute for generic q filter test as it's not trivially modifiable
973
786
  exclude_q_filter_predicates = ["query"]
974
787
 
@@ -1001,6 +814,7 @@ class GraphQLTestCase(FilterTestCases.NameOnlyFilterTestCase):
1001
814
  class ImageAttachmentTestCase(FilterTestCases.FilterTestCase):
1002
815
  queryset = ImageAttachment.objects.all()
1003
816
  filterset = ImageAttachmentFilterSet
817
+ generic_filter_tests = (("name",),)
1004
818
 
1005
819
  @classmethod
1006
820
  def setUpTestData(cls):
@@ -1048,10 +862,6 @@ class ImageAttachmentTestCase(FilterTestCases.FilterTestCase):
1048
862
  image_width=100,
1049
863
  )
1050
864
 
1051
- def test_name(self):
1052
- params = {"name": ["Image Attachment 1", "Image Attachment 2"]}
1053
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1054
-
1055
865
  def test_content_type(self):
1056
866
  params = {"content_type": "dcim.location"}
1057
867
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1064,25 +874,27 @@ class ImageAttachmentTestCase(FilterTestCases.FilterTestCase):
1064
874
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1065
875
 
1066
876
 
1067
- class JobFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
877
+ class JobFilterSetTestCase(FilterTestCases.FilterTestCase):
1068
878
  queryset = Job.objects.all()
1069
879
  filterset = JobFilterSet
880
+ generic_filter_tests = (
881
+ ("grouping",),
882
+ ("job_class_name",),
883
+ ("module_name",),
884
+ ("name",),
885
+ )
1070
886
 
1071
887
  @classmethod
1072
888
  def setUpTestData(cls):
1073
889
  Job.objects.first().tags.set(Tag.objects.get_for_model(Job))
1074
890
  Job.objects.last().tags.set(Tag.objects.get_for_model(Job)[:3])
1075
891
 
1076
- def test_grouping(self):
1077
- params = {"grouping": ["file_upload_pass", "file_upload_fail"]}
1078
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1079
-
1080
892
  def test_installed(self):
1081
- params = {"job_class_name": "TestPassJob", "installed": True}
893
+ params = {"job_class_name": "TestPass", "installed": True}
1082
894
  self.assertTrue(self.filterset(params, self.queryset).qs.exists())
1083
895
 
1084
896
  def test_enabled(self):
1085
- params = {"job_class_name": "TestPassJob", "enabled": False}
897
+ params = {"job_class_name": "TestPass", "enabled": False}
1086
898
  self.assertTrue(self.filterset(params, self.queryset).qs.exists())
1087
899
 
1088
900
  def test_dryrun_default(self):
@@ -1106,39 +918,85 @@ class JobFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
1106
918
  self.queryset.filter(approval_required=True),
1107
919
  )
1108
920
 
1109
- def test_search(self):
1110
- params = {"q": "file"}
1111
- expected_matches = (
1112
- Q(name__icontains="file") # pylint: disable=unsupported-binary-operation
1113
- | Q(grouping__icontains="file")
1114
- | Q(description__icontains="file")
921
+ def test_is_job_hook_receiver(self):
922
+ params = {"is_job_hook_receiver": True}
923
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
924
+
925
+
926
+ class JobQueueFilterSetTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
927
+ queryset = JobQueue.objects.all()
928
+ filterset = JobQueueFilterSet
929
+ tenancy_related_name = "job_queues"
930
+ generic_filter_tests = [
931
+ ["name"],
932
+ ]
933
+
934
+ @classmethod
935
+ def setUpTestData(cls):
936
+ # create some job queues that do not have jobs attached to them
937
+ # for has_jobs boolean filter
938
+ JobQueue.objects.create(
939
+ name="Empty Job Queue 1",
940
+ queue_type=JobQueueTypeChoices.TYPE_KUBERNETES,
941
+ )
942
+ JobQueue.objects.create(name="Empty Job Queue 2", queue_type=JobQueueTypeChoices.TYPE_CELERY)
943
+ JobQueue.objects.create(
944
+ name="Empty Job Queue 3",
945
+ queue_type=JobQueueTypeChoices.TYPE_KUBERNETES,
946
+ )
947
+ JobQueue.objects.create(
948
+ name="Empty Job Queue 4",
949
+ queue_type=JobQueueTypeChoices.TYPE_KUBERNETES,
950
+ )
951
+
952
+ def test_queue_type(self):
953
+ # we cannot add this test to self.generic_filter_tests because JobQueueTypeChoices only has two values.
954
+ # self.generic_filter_tests needs at least three.
955
+ params = {"queue_type": [JobQueueTypeChoices.TYPE_CELERY]}
956
+ self.assertQuerysetEqualAndNotEmpty(
957
+ self.filterset(params, self.queryset).qs, self.queryset.filter(queue_type=JobQueueTypeChoices.TYPE_CELERY)
1115
958
  )
959
+ params = {"queue_type": [JobQueueTypeChoices.TYPE_KUBERNETES]}
1116
960
  self.assertQuerysetEqualAndNotEmpty(
1117
- self.filterset(params, self.queryset).qs, self.queryset.filter(expected_matches)
961
+ self.filterset(params, self.queryset).qs,
962
+ self.queryset.filter(queue_type=JobQueueTypeChoices.TYPE_KUBERNETES),
1118
963
  )
1119
- value = self.queryset.values_list("pk", flat=True)[0]
1120
- params = {"q": value}
1121
- self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
1122
964
 
1123
- def test_is_job_hook_receiver(self):
1124
- params = {"is_job_hook_receiver": True}
1125
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
965
+
966
+ class JobQueueAssignmentFilterSetTestCase(FilterTestCases.FilterTestCase):
967
+ queryset = JobQueueAssignment.objects.all()
968
+ filterset = JobQueueAssignmentFilterSet
969
+ generic_filter_tests = [
970
+ ("job", "job__id"),
971
+ ("job", "job__name"),
972
+ ("job_queue", "job_queue__id"),
973
+ ("job_queue", "job_queue__name"),
974
+ ]
1126
975
 
1127
976
 
1128
977
  class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
1129
978
  queryset = JobResult.objects.all()
1130
979
  filterset = JobResultFilterSet
980
+ generic_filter_tests = (
981
+ ("date_created",),
982
+ ("date_done",),
983
+ ("job_model", "job_model__id"),
984
+ ("job_model", "job_model__name"),
985
+ ("job_model_id", "job_model__id"),
986
+ ("name",),
987
+ ("status",),
988
+ )
1131
989
 
1132
990
  @classmethod
1133
991
  def setUpTestData(cls):
1134
992
  jobs = Job.objects.all()[:3]
1135
993
  cls.jobs = jobs
1136
994
  user = User.objects.create(username="user1", is_active=True)
1137
- job_model = Job.objects.get_for_class_path("pass.TestPassJob")
995
+ job_model = Job.objects.get_for_class_path("pass.TestPass")
1138
996
  scheduled_jobs = [
1139
997
  ScheduledJob.objects.create(
1140
998
  name="test1",
1141
- task="pass.TestPassJob",
999
+ task="pass.TestPass",
1142
1000
  job_model=job_model,
1143
1001
  interval=JobExecutionType.TYPE_IMMEDIATELY,
1144
1002
  user=user,
@@ -1147,7 +1005,7 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
1147
1005
  ),
1148
1006
  ScheduledJob.objects.create(
1149
1007
  name="test2",
1150
- task="pass.TestPassJob",
1008
+ task="pass.TestPass",
1151
1009
  job_model=job_model,
1152
1010
  interval=JobExecutionType.TYPE_DAILY,
1153
1011
  user=user,
@@ -1157,7 +1015,7 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
1157
1015
  ),
1158
1016
  ScheduledJob.objects.create(
1159
1017
  name="test3",
1160
- task="pass.TestPassJob",
1018
+ task="pass.TestPass",
1161
1019
  job_model=job_model,
1162
1020
  interval=JobExecutionType.TYPE_CUSTOM,
1163
1021
  crontab="34 12 * * *",
@@ -1178,17 +1036,6 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
1178
1036
  scheduled_job=scheduled_jobs[idx],
1179
1037
  )
1180
1038
 
1181
- def test_job_model(self):
1182
- jobs = list(self.jobs[:2])
1183
- filter_params = [
1184
- {"job_model_id": [jobs[0].pk, jobs[1].pk]},
1185
- {"job_model": [jobs[0].pk, jobs[1].name]},
1186
- ]
1187
- for params in filter_params:
1188
- self.assertQuerysetEqualAndNotEmpty(
1189
- self.filterset(params, self.queryset).qs, self.queryset.filter(job_model__in=jobs).distinct()
1190
- )
1191
-
1192
1039
  def test_scheduled_job(self):
1193
1040
  scheduled_jobs = list(self.scheduled_jobs[:2])
1194
1041
  filter_params = [
@@ -1201,9 +1048,14 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
1201
1048
  )
1202
1049
 
1203
1050
 
1204
- class JobHookFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
1051
+ class JobHookFilterSetTestCase(FilterTestCases.FilterTestCase):
1205
1052
  queryset = JobHook.objects.all()
1206
1053
  filterset = JobHookFilterSet
1054
+ generic_filter_tests = (
1055
+ ("job", "job__id"),
1056
+ ("job", "job__name"),
1057
+ ("name",),
1058
+ )
1207
1059
 
1208
1060
  @classmethod
1209
1061
  def setUpTestData(cls):
@@ -1234,10 +1086,6 @@ class JobHookFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
1234
1086
  job_hooks[0].content_types.set([devicetype_ct])
1235
1087
  job_hooks[1].content_types.set([devicetype_ct])
1236
1088
 
1237
- def test_name(self):
1238
- params = {"name": ["JobHook1", "JobHook2"]}
1239
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1240
-
1241
1089
  def test_content_types(self):
1242
1090
  params = {"content_types": ["dcim.devicetype"]}
1243
1091
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1246,11 +1094,6 @@ class JobHookFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
1246
1094
  params = {"enabled": True}
1247
1095
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1248
1096
 
1249
- def test_job(self):
1250
- jobs = Job.objects.filter(job_class_name__in=["TestJobHookReceiverLog", "TestJobHookReceiverChange"])[:2]
1251
- params = {"job": [jobs[0].name, jobs[1].pk]}
1252
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1253
-
1254
1097
  def test_type_create(self):
1255
1098
  params = {"type_create": True}
1256
1099
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1263,16 +1106,17 @@ class JobHookFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
1263
1106
  params = {"type_update": True}
1264
1107
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1265
1108
 
1266
- def test_search(self):
1267
- params = {"q": "hook"}
1268
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
1269
- params = {"q": "hook1"}
1270
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1271
-
1272
1109
 
1273
1110
  class JobButtonFilterTestCase(FilterTestCases.FilterTestCase):
1274
1111
  queryset = JobButton.objects.all()
1275
1112
  filterset = JobButtonFilterSet
1113
+ generic_filter_tests = (
1114
+ # ("job", "job__id"), # TODO: not enough distinct values for generic test
1115
+ # ("job", "job__name"), # TODO: not enough distinct values for generic test
1116
+ ("name",),
1117
+ ("text",),
1118
+ ("weight",),
1119
+ )
1276
1120
 
1277
1121
  @classmethod
1278
1122
  def setUpTestData(cls):
@@ -1304,12 +1148,6 @@ class JobButtonFilterTestCase(FilterTestCases.FilterTestCase):
1304
1148
  for jb in job_buttons:
1305
1149
  jb.content_types.set([location_ct])
1306
1150
 
1307
- def test_name(self):
1308
- params = {"name": ["JobButton1", "JobButton2"]}
1309
- self.assertQuerysetEqualAndNotEmpty(
1310
- self.filterset(params, self.queryset).qs, self.queryset.filter(name__in=["JobButton1", "JobButton2"])
1311
- )
1312
-
1313
1151
  def test_job(self):
1314
1152
  job = Job.objects.get(job_class_name="TestJobButtonReceiverSimple")
1315
1153
  params = {"job": [job.pk]}
@@ -1322,23 +1160,15 @@ class JobButtonFilterTestCase(FilterTestCases.FilterTestCase):
1322
1160
  self.filterset(params, self.queryset).qs, self.queryset.filter(job__name=job.name)
1323
1161
  )
1324
1162
 
1325
- def test_weight(self):
1326
- params = {"weight": [30, 50]}
1327
- self.assertQuerysetEqualAndNotEmpty(
1328
- self.filterset(params, self.queryset).qs, self.queryset.filter(weight__in=[30, 50])
1329
- )
1330
-
1331
- def test_search(self):
1332
- params = {"q": "JobButton"}
1333
- self.assertQuerysetEqualAndNotEmpty(
1334
- self.filterset(params, self.queryset).qs,
1335
- self.queryset.filter(name__in=["JobButton1", "JobButton2", "JobButton3"]),
1336
- )
1337
-
1338
1163
 
1339
1164
  class JobLogEntryTestCase(FilterTestCases.FilterTestCase):
1340
1165
  queryset = JobLogEntry.objects.all()
1341
1166
  filterset = JobLogEntryFilterSet
1167
+ generic_filter_tests = (
1168
+ ("grouping",),
1169
+ ("log_level",),
1170
+ ("message",),
1171
+ )
1342
1172
 
1343
1173
  @classmethod
1344
1174
  def setUpTestData(cls):
@@ -1352,56 +1182,6 @@ class JobLogEntryTestCase(FilterTestCases.FilterTestCase):
1352
1182
  message=f"I am a {log_level} log.",
1353
1183
  )
1354
1184
 
1355
- def test_log_level(self):
1356
- params = {"log_level": ["debug"]}
1357
- self.assertQuerysetEqualAndNotEmpty(
1358
- self.filterset(params, self.queryset).qs, self.queryset.filter(log_level="debug")
1359
- )
1360
-
1361
- def test_grouping(self):
1362
- params = {"grouping": ["run"]}
1363
- self.assertQuerysetEqualAndNotEmpty(
1364
- self.filterset(params, self.queryset).qs, self.queryset.filter(grouping="run")
1365
- )
1366
-
1367
- def test_message(self):
1368
- params = {"message": ["I am a debug log."]}
1369
- self.assertQuerysetEqualAndNotEmpty(
1370
- self.filterset(params, self.queryset).qs, self.queryset.filter(message="I am a debug log.")
1371
- )
1372
-
1373
- def test_search(self):
1374
- params = {"q": "run"}
1375
- self.assertQuerysetEqualAndNotEmpty(
1376
- self.filterset(params, self.queryset).qs,
1377
- self.queryset.filter(
1378
- # TODO: Remove pylint disable after issue is resolved (see: https://github.com/PyCQA/pylint/issues/7381)
1379
- # pylint: disable=unsupported-binary-operation
1380
- Q(grouping__icontains="run") | Q(message__icontains="run") | Q(log_level__icontains="run")
1381
- # pylint: enable=unsupported-binary-operation
1382
- ),
1383
- )
1384
- params = {"q": "warning"}
1385
- self.assertQuerysetEqualAndNotEmpty(
1386
- self.filterset(params, self.queryset).qs,
1387
- self.queryset.filter(
1388
- # TODO: Remove pylint disable after issue is resolved (see: https://github.com/PyCQA/pylint/issues/7381)
1389
- # pylint: disable=unsupported-binary-operation
1390
- Q(grouping__icontains="warning") | Q(message__icontains="warning") | Q(log_level__icontains="warning")
1391
- # pylint: enable=unsupported-binary-operation
1392
- ),
1393
- )
1394
- params = {"q": "debug"}
1395
- self.assertQuerysetEqualAndNotEmpty(
1396
- self.filterset(params, self.queryset).qs,
1397
- self.queryset.filter(
1398
- # TODO: Remove pylint disable after issue is resolved (see: https://github.com/PyCQA/pylint/issues/7381)
1399
- # pylint: disable=unsupported-binary-operation
1400
- Q(grouping__icontains="debug") | Q(message__icontains="debug") | Q(log_level__icontains="debug")
1401
- # pylint: enable=unsupported-binary-operation
1402
- ),
1403
- )
1404
-
1405
1185
 
1406
1186
  class MetadataChoiceTestCase(FilterTestCases.FilterTestCase):
1407
1187
  queryset = MetadataChoice.objects.all()
@@ -1434,6 +1214,12 @@ class MetadataTypeTestCase(FilterTestCases.FilterTestCase):
1434
1214
  class ObjectChangeTestCase(FilterTestCases.FilterTestCase):
1435
1215
  queryset = ObjectChange.objects.all()
1436
1216
  filterset = ObjectChangeFilterSet
1217
+ generic_filter_tests = (
1218
+ ("user", "user__id"),
1219
+ ("user", "user__username"),
1220
+ ("user_id", "user__id"),
1221
+ ("user_name",),
1222
+ )
1437
1223
 
1438
1224
  @classmethod
1439
1225
  def setUpTestData(cls):
@@ -1505,21 +1291,6 @@ class ObjectChangeTestCase(FilterTestCases.FilterTestCase):
1505
1291
  object_data={"address": str(ipaddress.address), "status": str(ipaddress.status)},
1506
1292
  )
1507
1293
 
1508
- def test_user(self):
1509
- users = list(User.objects.filter(username__in=["user1", "user2"]))
1510
- filter_params = [
1511
- {"user_id": [users[0].pk, users[1].pk]},
1512
- {"user": [users[0].pk, users[1].username]},
1513
- ]
1514
- for params in filter_params:
1515
- self.assertQuerysetEqualAndNotEmpty(
1516
- self.filterset(params, self.queryset).qs, self.queryset.filter(user__in=users).distinct()
1517
- )
1518
-
1519
- def test_user_name(self):
1520
- params = {"user_name": ["user1", "user2"]}
1521
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
1522
-
1523
1294
  def test_changed_object_type(self):
1524
1295
  params = {"changed_object_type": "dcim.location"}
1525
1296
  self.assertQuerysetEqualAndNotEmpty(
@@ -1534,11 +1305,6 @@ class ObjectChangeTestCase(FilterTestCases.FilterTestCase):
1534
1305
  self.queryset.filter(changed_object_type=ContentType.objects.get_for_model(Location)),
1535
1306
  )
1536
1307
 
1537
- def test_search(self):
1538
- value = self.queryset.values_list("pk", flat=True)[0]
1539
- params = {"q": value}
1540
- self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
1541
-
1542
1308
 
1543
1309
  class ObjectMetadataTestCase(FilterTestCases.FilterTestCase):
1544
1310
  queryset = ObjectMetadata.objects.all()
@@ -1618,6 +1384,11 @@ class ObjectMetadataTestCase(FilterTestCases.FilterTestCase):
1618
1384
  class RelationshipTestCase(FilterTestCases.FilterTestCase):
1619
1385
  queryset = Relationship.objects.all()
1620
1386
  filterset = RelationshipFilterSet
1387
+ generic_filter_tests = (
1388
+ ("key",),
1389
+ ("label",),
1390
+ ("type",),
1391
+ )
1621
1392
 
1622
1393
  @classmethod
1623
1394
  def setUpTestData(cls):
@@ -1647,26 +1418,6 @@ class RelationshipTestCase(FilterTestCases.FilterTestCase):
1647
1418
  destination_type=interface_type,
1648
1419
  ).validated_save()
1649
1420
 
1650
- def test_label(self):
1651
- """Verify that the filterset supports filtering by label."""
1652
- params = {"label": list(self.queryset.values_list("label", flat=True)[:2])}
1653
- filterset = self.filterset(params, self.queryset)
1654
- self.assertTrue(filterset.is_valid())
1655
- self.assertQuerysetEqualAndNotEmpty(
1656
- filterset.qs.order_by("label"), self.queryset.filter(label__in=params["label"]).order_by("label")
1657
- )
1658
-
1659
- def test_key(self):
1660
- """Verify that the filterset supports filtering by key."""
1661
- params = {"key": self.queryset.values_list("key", flat=True)[:2]}
1662
- filterset = self.filterset(params, self.queryset)
1663
- self.assertTrue(filterset.is_valid())
1664
- self.assertEqual(filterset.qs.count(), 2)
1665
-
1666
- def test_type(self):
1667
- params = {"type": ["one-to-many"]}
1668
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1669
-
1670
1421
  def test_source_type(self):
1671
1422
  params = {"source_type": ["dcim.device"]}
1672
1423
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1679,6 +1430,12 @@ class RelationshipTestCase(FilterTestCases.FilterTestCase):
1679
1430
  class RelationshipAssociationFilterSetTestCase(FilterTestCases.FilterTestCase):
1680
1431
  queryset = RelationshipAssociation.objects.all()
1681
1432
  filterset = RelationshipAssociationFilterSet
1433
+ generic_filter_tests = (
1434
+ ("destination_id",),
1435
+ # ("relationship", "relationship__id"), # TODO?
1436
+ ("relationship", "relationship__key"),
1437
+ ("source_id",),
1438
+ )
1682
1439
 
1683
1440
  @classmethod
1684
1441
  def setUpTestData(cls):
@@ -1794,26 +1551,14 @@ class RelationshipAssociationFilterSetTestCase(FilterTestCases.FilterTestCase):
1794
1551
  for relationship_association in cls.relationship_associations:
1795
1552
  relationship_association.validated_save()
1796
1553
 
1797
- def test_relationship(self):
1798
- params = {"relationship": [self.relationships[0].key]}
1799
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1800
-
1801
1554
  def test_source_type(self):
1802
1555
  params = {"source_type": ["dcim.device", "dcim.interface"]}
1803
1556
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
1804
1557
 
1805
- def test_source_id(self):
1806
- params = {"source_id": [self.devices[0].pk, self.devices[1].pk]}
1807
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
1808
-
1809
1558
  def test_destination_type(self):
1810
1559
  params = {"destination_type": ["dcim.device", "dcim.interface"]}
1811
1560
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
1812
1561
 
1813
- def test_destination_id(self):
1814
- params = {"destination_id": [self.devices[0].pk, self.devices[1].pk]}
1815
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
1816
-
1817
1562
  def test_peer_id(self):
1818
1563
  params = {"peer_id": [self.devices[0].pk, self.devices[1].pk]}
1819
1564
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@@ -1949,9 +1694,16 @@ class SavedViewTestCase(FilterTestCases.FilterTestCase):
1949
1694
  )
1950
1695
 
1951
1696
 
1952
- class SecretTestCase(FilterTestCases.NameOnlyFilterTestCase):
1697
+ class SecretTestCase(FilterTestCases.FilterTestCase):
1953
1698
  queryset = Secret.objects.all()
1954
1699
  filterset = SecretFilterSet
1700
+ generic_filter_tests = (
1701
+ ("created",),
1702
+ ("last_updated",),
1703
+ ("name",),
1704
+ ("tags", "tags__id"),
1705
+ ("tags", "tags__name"),
1706
+ )
1955
1707
 
1956
1708
  @classmethod
1957
1709
  def setUpTestData(cls):
@@ -1981,15 +1733,15 @@ class SecretTestCase(FilterTestCases.NameOnlyFilterTestCase):
1981
1733
  params = {"provider": ["environment-variable"]}
1982
1734
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1983
1735
 
1984
- def test_search(self):
1985
- value = self.queryset.values_list("pk", flat=True)[0]
1986
- params = {"q": value}
1987
- self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
1988
-
1989
1736
 
1990
- class SecretsGroupTestCase(FilterTestCases.NameOnlyFilterTestCase):
1737
+ class SecretsGroupTestCase(FilterTestCases.FilterTestCase):
1991
1738
  queryset = SecretsGroup.objects.all()
1992
1739
  filterset = SecretsGroupFilterSet
1740
+ generic_filter_tests = (
1741
+ ("created",),
1742
+ ("last_updated",),
1743
+ ("name",),
1744
+ )
1993
1745
 
1994
1746
  @classmethod
1995
1747
  def setUpTestData(cls):
@@ -1997,17 +1749,18 @@ class SecretsGroupTestCase(FilterTestCases.NameOnlyFilterTestCase):
1997
1749
  SecretsGroup.objects.create(name="Group 2")
1998
1750
  SecretsGroup.objects.create(name="Group 3")
1999
1751
 
2000
- def test_search(self):
2001
- value = self.queryset.values_list("pk", flat=True)[0]
2002
- params = {"q": value}
2003
- self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
2004
-
2005
1752
 
2006
1753
  class SecretsGroupAssociationTestCase(FilterTestCases.FilterTestCase):
2007
1754
  queryset = SecretsGroupAssociation.objects.all()
2008
1755
  filterset = SecretsGroupAssociationFilterSet
2009
1756
 
2010
- generic_filter_tests = (["secrets_group", "secrets_group__id"],)
1757
+ generic_filter_tests = (
1758
+ ("secret", "secret__id"),
1759
+ ("secret", "secret__name"),
1760
+ ("secret_id", "secret__id"),
1761
+ ("secrets_group", "secrets_group__id"),
1762
+ ("secrets_group", "secrets_group__name"),
1763
+ )
2011
1764
 
2012
1765
  @classmethod
2013
1766
  def setUpTestData(cls):
@@ -2057,16 +1810,6 @@ class SecretsGroupAssociationTestCase(FilterTestCases.FilterTestCase):
2057
1810
  secret_type=SecretsGroupSecretTypeChoices.TYPE_PASSWORD,
2058
1811
  )
2059
1812
 
2060
- def test_secret(self):
2061
- filter_params = [
2062
- {"secret_id": [self.secrets[0].pk, self.secrets[1].pk]},
2063
- {"secret": [self.secrets[0].name, self.secrets[1].pk]},
2064
- ]
2065
- for params in filter_params:
2066
- self.assertQuerysetEqualAndNotEmpty(
2067
- self.filterset(params, self.queryset).qs, self.queryset.filter(secret__in=self.secrets[:2]).distinct()
2068
- )
2069
-
2070
1813
  def test_access_type(self):
2071
1814
  params = {"access_type": [SecretsGroupAccessTypeChoices.TYPE_GENERIC]}
2072
1815
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -2103,9 +1846,13 @@ class StaticGroupAssociationTestCase(FilterTestCases.FilterTestCase):
2103
1846
  )
2104
1847
 
2105
1848
 
2106
- class StatusTestCase(FilterTestCases.NameOnlyFilterTestCase):
1849
+ class StatusTestCase(FilterTestCases.FilterTestCase):
2107
1850
  queryset = Status.objects.all()
2108
1851
  filterset = StatusFilterSet
1852
+ generic_filter_tests = (
1853
+ ("color",),
1854
+ ("name",),
1855
+ )
2109
1856
 
2110
1857
  def test_content_types(self):
2111
1858
  ct = ContentType.objects.get_for_model(Device)
@@ -2113,57 +1860,25 @@ class StatusTestCase(FilterTestCases.NameOnlyFilterTestCase):
2113
1860
  params = {"content_types": ["dcim.device"]}
2114
1861
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), status_count)
2115
1862
 
2116
- def test_color(self):
2117
- """Test the color search field."""
2118
- params = {"color": [ColorChoices.COLOR_GREY]}
2119
- self.assertQuerysetEqualAndNotEmpty(
2120
- self.filterset(params, self.queryset).qs, Status.objects.filter(color=ColorChoices.COLOR_GREY)
2121
- )
2122
-
2123
- def test_search(self):
2124
- params = {"q": "active"}
2125
- # TODO: Remove pylint disable after issue is resolved (see: https://github.com/PyCQA/pylint/issues/7381)
2126
- # pylint: disable=unsupported-binary-operation
2127
- q = Q(id__iexact="active") | Q(name__icontains="active")
2128
- # pylint: enable=unsupported-binary-operation
2129
- q |= Q(content_types__model__icontains="active")
2130
- self.assertQuerysetEqualAndNotEmpty(
2131
- self.filterset(params, self.queryset).qs,
2132
- self.queryset.filter(q).distinct(),
2133
- )
2134
- value = self.queryset.first().pk
2135
- params = {"q": value}
2136
- self.assertQuerysetEqualAndNotEmpty(
2137
- self.filterset(params, self.queryset).qs,
2138
- self.queryset.filter(pk=value),
2139
- )
2140
-
2141
1863
 
2142
- class TagTestCase(FilterTestCases.NameOnlyFilterTestCase):
1864
+ class TagTestCase(FilterTestCases.FilterTestCase):
2143
1865
  queryset = Tag.objects.all()
2144
1866
  filterset = TagFilterSet
1867
+ generic_filter_tests = (
1868
+ ("color",),
1869
+ ("name",),
1870
+ )
2145
1871
 
2146
1872
  @classmethod
2147
1873
  def setUpTestData(cls):
2148
1874
  cls.tags = Tag.objects.all()
2149
1875
 
2150
- def test_color(self):
2151
- params = {"color": [self.tags[0].color, self.tags[1].color]}
2152
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
2153
-
2154
1876
  def test_content_types(self):
2155
1877
  params = {"content_types": ["dcim.location"]}
2156
1878
  filtered_data = self.filterset(params, self.queryset).qs
2157
1879
  self.assertQuerysetEqual(filtered_data, Tag.objects.get_for_model(Location))
2158
1880
  self.assertEqual(filtered_data[0], Tag.objects.get_for_model(Location)[0])
2159
1881
 
2160
- def test_search(self):
2161
- params = {"q": self.tags[0].name}
2162
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2163
- value = self.queryset.values_list("pk", flat=True)[0]
2164
- params = {"q": value}
2165
- self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
2166
-
2167
1882
 
2168
1883
  class TeamFilterSetTestCase(ContactAndTeamFilterSetTestCaseMixin, FilterTestCases.FilterTestCase):
2169
1884
  queryset = Team.objects.all()
@@ -2181,6 +1896,10 @@ class TeamFilterSetTestCase(ContactAndTeamFilterSetTestCaseMixin, FilterTestCase
2181
1896
  class WebhookTestCase(FilterTestCases.FilterTestCase):
2182
1897
  queryset = Webhook.objects.all()
2183
1898
  filterset = WebhookFilterSet
1899
+ generic_filter_tests = (
1900
+ ("name",),
1901
+ ("payload_url",),
1902
+ )
2184
1903
 
2185
1904
  @classmethod
2186
1905
  def setUpTestData(cls):
@@ -2212,10 +1931,6 @@ class WebhookTestCase(FilterTestCases.FilterTestCase):
2212
1931
  webhook.save()
2213
1932
  webhook.content_types.set([obj_type])
2214
1933
 
2215
- def test_name(self):
2216
- params = {"name": ["webhook-1"]}
2217
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2218
-
2219
1934
  def test_create(self):
2220
1935
  params = {"type_create": True}
2221
1936
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -2232,17 +1947,15 @@ class WebhookTestCase(FilterTestCases.FilterTestCase):
2232
1947
  params = {"enabled": True}
2233
1948
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
2234
1949
 
2235
- def test_search(self):
2236
- params = {"q": "webhook"}
2237
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
2238
- value = self.queryset.values_list("pk", flat=True)[0]
2239
- params = {"q": value}
2240
- self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
2241
-
2242
1950
 
2243
- class RoleTestCase(FilterTestCases.NameOnlyFilterTestCase):
1951
+ class RoleTestCase(FilterTestCases.FilterTestCase):
2244
1952
  queryset = Role.objects.all()
2245
1953
  filterset = RoleFilterSet
1954
+ generic_filter_tests = (
1955
+ ("color",),
1956
+ ("name",),
1957
+ ("weight",),
1958
+ )
2246
1959
 
2247
1960
  def test_content_types(self):
2248
1961
  device_ct = ContentType.objects.get_for_model(Device)
@@ -2254,32 +1967,3 @@ class RoleTestCase(FilterTestCases.NameOnlyFilterTestCase):
2254
1967
  rack_roles = self.queryset.filter(content_types=rack_ct)
2255
1968
  params = {"content_types": ["dcim.rack"]}
2256
1969
  self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, rack_roles)
2257
-
2258
- def test_color(self):
2259
- """Test the color search field."""
2260
- params = {"color": [ColorChoices.COLOR_AMBER]}
2261
- self.assertQuerysetEqualAndNotEmpty(
2262
- self.filterset(params, self.queryset).qs, Role.objects.filter(color=ColorChoices.COLOR_AMBER)
2263
- )
2264
-
2265
- def test_weight(self):
2266
- """Test the weight search field."""
2267
- instance = self.queryset.filter(weight__isnull=False).first()
2268
- params = {"weight": [instance.weight]}
2269
- self.assertQuerysetEqualAndNotEmpty(
2270
- self.filterset(params, self.queryset).qs, self.queryset.filter(weight=instance.weight)
2271
- )
2272
-
2273
- def test_search(self):
2274
- value = self.queryset.first().name
2275
- params = {"q": value}
2276
- self.assertQuerysetEqualAndNotEmpty(
2277
- self.filterset(params, self.queryset).qs,
2278
- self.queryset.filter(name__icontains=value).distinct(),
2279
- )
2280
- value = self.queryset.first().pk
2281
- params = {"q": value}
2282
- self.assertQuerysetEqualAndNotEmpty(
2283
- self.filterset(params, self.queryset).qs,
2284
- self.queryset.filter(pk=value),
2285
- )