nautobot 2.2.7__py3-none-any.whl → 2.2.9__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 (306) hide show
  1. nautobot/core/filters.py +15 -1
  2. nautobot/core/graphql/generators.py +4 -4
  3. nautobot/core/graphql/schema.py +9 -7
  4. nautobot/core/jobs/__init__.py +4 -1
  5. nautobot/core/settings.py +13 -2
  6. nautobot/core/settings.yaml +14 -0
  7. nautobot/core/templates/nautobot_config.py.j2 +15 -0
  8. nautobot/core/tests/integration/test_general_functionality.py +36 -0
  9. nautobot/core/tests/test_graphql.py +51 -5
  10. nautobot/core/tests/test_jobs.py +82 -2
  11. nautobot/core/tests/test_templatetags_netutils.py +3 -3
  12. nautobot/dcim/models/device_components.py +7 -0
  13. nautobot/dcim/models/devices.py +14 -3
  14. nautobot/dcim/tables/devices.py +19 -4
  15. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  16. nautobot/dcim/tests/test_models.py +106 -0
  17. nautobot/dcim/tests/test_views.py +13 -0
  18. nautobot/dcim/views.py +8 -2
  19. nautobot/extras/api/views.py +7 -59
  20. nautobot/extras/homepage.py +12 -2
  21. nautobot/extras/jobs.py +2 -2
  22. nautobot/extras/models/jobs.py +81 -0
  23. nautobot/extras/models/relationships.py +12 -0
  24. nautobot/extras/signals.py +14 -1
  25. nautobot/extras/tables.py +36 -5
  26. nautobot/extras/templates/extras/job_detail.html +11 -0
  27. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  28. nautobot/extras/tests/test_relationships.py +221 -1
  29. nautobot/extras/tests/test_views.py +21 -0
  30. nautobot/extras/utils.py +34 -5
  31. nautobot/extras/views.py +20 -46
  32. nautobot/ipam/models.py +9 -12
  33. nautobot/ipam/tests/test_models.py +3 -2
  34. nautobot/ipam/views.py +2 -8
  35. nautobot/project-static/css/base.css +1 -0
  36. nautobot/project-static/docs/404.html +4 -4
  37. nautobot/project-static/docs/apps/index.html +4 -4
  38. nautobot/project-static/docs/apps/nautobot-apps.html +4 -4
  39. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css → main.76a95c52.min.css} +1 -1
  40. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css.map → main.76a95c52.min.css.map} +1 -1
  41. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +4 -4
  42. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +4 -4
  43. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +4 -4
  44. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +4 -4
  45. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +4 -4
  46. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +4 -4
  47. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +4 -4
  48. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +4 -4
  49. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +4 -4
  50. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +4 -4
  51. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +4 -4
  52. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
  53. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +4 -4
  54. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +4 -4
  55. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +4 -4
  56. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +4 -4
  57. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +4 -4
  58. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +4 -4
  59. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +4 -4
  60. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +4 -4
  61. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +4 -4
  62. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +6 -4
  63. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +4 -4
  64. nautobot/project-static/docs/development/apps/api/configuration-view.html +4 -4
  65. nautobot/project-static/docs/development/apps/api/database-backend-config.html +4 -4
  66. nautobot/project-static/docs/development/apps/api/models/django-admin.html +4 -4
  67. nautobot/project-static/docs/development/apps/api/models/global-search.html +4 -4
  68. nautobot/project-static/docs/development/apps/api/models/graphql.html +4 -4
  69. nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
  70. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +4 -4
  71. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +4 -4
  72. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +4 -4
  73. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +4 -4
  74. nautobot/project-static/docs/development/apps/api/platform-features/index.html +4 -4
  75. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +4 -4
  76. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +4 -4
  77. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +4 -4
  78. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +4 -4
  79. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +4 -4
  80. nautobot/project-static/docs/development/apps/api/prometheus.html +4 -4
  81. nautobot/project-static/docs/development/apps/api/setup.html +4 -4
  82. nautobot/project-static/docs/development/apps/api/testing.html +4 -4
  83. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +4 -4
  84. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +4 -4
  85. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +4 -4
  86. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +4 -4
  87. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +4 -4
  88. nautobot/project-static/docs/development/apps/api/views/base-template.html +4 -4
  89. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +4 -4
  90. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +4 -4
  91. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +4 -4
  92. nautobot/project-static/docs/development/apps/api/views/index.html +4 -4
  93. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +4 -4
  94. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +4 -4
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +4 -4
  96. nautobot/project-static/docs/development/apps/api/views/notes.html +4 -4
  97. nautobot/project-static/docs/development/apps/api/views/rest-api.html +4 -4
  98. nautobot/project-static/docs/development/apps/api/views/urls.html +4 -4
  99. nautobot/project-static/docs/development/apps/index.html +4 -4
  100. nautobot/project-static/docs/development/apps/migration/code-updates.html +4 -4
  101. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +4 -4
  102. nautobot/project-static/docs/development/apps/migration/from-v1.html +4 -4
  103. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +4 -4
  104. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +4 -4
  105. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +4 -4
  106. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +4 -4
  107. nautobot/project-static/docs/development/apps/porting-from-netbox.html +4 -4
  108. nautobot/project-static/docs/development/core/application-registry.html +4 -4
  109. nautobot/project-static/docs/development/core/best-practices.html +4 -4
  110. nautobot/project-static/docs/development/core/bootstrap-ui.html +4 -4
  111. nautobot/project-static/docs/development/core/caching.html +4 -4
  112. nautobot/project-static/docs/development/core/controllers.html +4 -4
  113. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +4 -4
  114. nautobot/project-static/docs/development/core/generic-views.html +4 -4
  115. nautobot/project-static/docs/development/core/getting-started.html +4 -4
  116. nautobot/project-static/docs/development/core/homepage.html +4 -4
  117. nautobot/project-static/docs/development/core/index.html +15 -4
  118. nautobot/project-static/docs/development/core/model-checklist.html +4 -4
  119. nautobot/project-static/docs/development/core/model-features.html +4 -4
  120. nautobot/project-static/docs/development/core/natural-keys.html +4 -4
  121. nautobot/project-static/docs/development/core/navigation-menu.html +4 -4
  122. nautobot/project-static/docs/development/core/release-checklist.html +4 -4
  123. nautobot/project-static/docs/development/core/role-internals.html +4 -4
  124. nautobot/project-static/docs/development/core/settings.html +4 -4
  125. nautobot/project-static/docs/development/core/style-guide.html +4 -4
  126. nautobot/project-static/docs/development/core/templates.html +4 -4
  127. nautobot/project-static/docs/development/core/testing.html +4 -4
  128. nautobot/project-static/docs/development/core/user-preferences.html +4 -4
  129. nautobot/project-static/docs/development/index.html +4 -4
  130. nautobot/project-static/docs/development/jobs/index.html +379 -365
  131. nautobot/project-static/docs/development/jobs/migration/from-v1.html +4 -4
  132. nautobot/project-static/docs/index.html +8228 -13
  133. nautobot/project-static/docs/overview/application_stack.html +4 -4
  134. nautobot/project-static/docs/overview/design_philosophy.html +6 -6
  135. nautobot/project-static/docs/overview/index.html +13 -8228
  136. nautobot/project-static/docs/release-notes/index.html +4 -4
  137. nautobot/project-static/docs/release-notes/version-1.0.html +4 -4
  138. nautobot/project-static/docs/release-notes/version-1.1.html +4 -4
  139. nautobot/project-static/docs/release-notes/version-1.2.html +4 -4
  140. nautobot/project-static/docs/release-notes/version-1.3.html +4 -4
  141. nautobot/project-static/docs/release-notes/version-1.4.html +4 -4
  142. nautobot/project-static/docs/release-notes/version-1.5.html +4 -4
  143. nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
  144. nautobot/project-static/docs/release-notes/version-2.0.html +4 -4
  145. nautobot/project-static/docs/release-notes/version-2.1.html +4 -4
  146. nautobot/project-static/docs/release-notes/version-2.2.html +419 -136
  147. nautobot/project-static/docs/requirements.txt +2 -1
  148. nautobot/project-static/docs/search/search_index.json +1 -1
  149. nautobot/project-static/docs/sitemap.xml +260 -260
  150. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  151. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +4 -4
  152. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +4 -4
  153. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  154. nautobot/project-static/docs/user-guide/administration/configuration/index.html +4 -4
  155. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +36 -4
  156. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +4 -4
  157. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +4 -4
  158. nautobot/project-static/docs/user-guide/administration/guides/caching.html +4 -4
  159. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +8 -4
  160. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +4 -4
  161. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +4 -4
  162. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  163. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +4 -4
  164. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +4 -4
  165. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +4 -4
  166. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +4 -4
  167. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +4 -4
  168. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +8 -8
  169. nautobot/project-static/docs/user-guide/administration/installation/index.html +4 -4
  170. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +9 -5
  171. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +4 -4
  172. nautobot/project-static/docs/user-guide/administration/installation/services.html +4 -4
  173. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +4 -4
  174. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +4 -4
  175. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +4 -4
  176. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +4 -4
  177. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +4 -4
  178. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +62 -10
  179. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +4 -4
  180. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +4 -4
  181. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +4 -4
  182. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +4 -4
  183. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +4 -4
  184. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +4 -4
  185. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +4 -4
  186. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +4 -4
  187. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +4 -4
  188. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +4 -4
  189. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +4 -4
  190. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +4 -4
  191. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +4 -4
  192. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +4 -4
  193. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +4 -4
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +4 -4
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +4 -4
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +4 -4
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +4 -4
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +4 -4
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +4 -4
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +4 -4
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +4 -4
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +4 -4
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +4 -4
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +4 -4
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +4 -4
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +4 -4
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +4 -4
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +4 -4
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +4 -4
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +4 -4
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +4 -4
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +4 -4
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +4 -4
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +4 -4
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +4 -4
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +4 -4
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +4 -4
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +4 -4
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +4 -4
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +4 -4
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +4 -4
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +4 -4
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +4 -4
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +4 -4
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +4 -4
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +4 -4
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +4 -4
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -4
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +4 -4
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +4 -4
  231. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +4 -4
  232. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +4 -4
  233. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +4 -4
  234. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +4 -4
  235. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +4 -4
  236. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +4 -4
  237. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +4 -4
  238. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +4 -4
  239. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +4 -4
  240. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +4 -4
  241. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +4 -4
  242. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +4 -4
  243. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +4 -4
  244. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +4 -4
  245. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +4 -4
  246. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +4 -4
  247. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +4 -4
  248. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +4 -4
  249. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +4 -4
  250. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +4 -4
  251. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +4 -4
  252. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +4 -4
  253. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +4 -4
  254. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +4 -4
  255. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +4 -4
  256. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +4 -4
  257. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +4 -4
  258. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
  259. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +4 -4
  260. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +4 -4
  261. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +4 -4
  262. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +4 -4
  263. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +4 -4
  264. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +4 -4
  265. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +4 -4
  266. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +4 -4
  267. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +4 -4
  268. nautobot/project-static/docs/user-guide/index.html +4 -4
  269. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +4 -4
  270. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +4 -4
  271. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +4 -4
  272. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +4 -4
  273. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +4 -4
  274. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +4 -4
  275. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +4 -4
  276. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +4 -4
  277. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +4 -4
  278. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +4 -4
  279. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +4 -4
  280. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +4 -4
  281. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +4 -4
  282. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +4 -4
  283. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +4 -4
  284. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
  285. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +4 -4
  286. nautobot/project-static/docs/user-guide/platform-functionality/note.html +4 -4
  287. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +4 -4
  288. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +4 -4
  289. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +4 -4
  290. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +4 -4
  291. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +4 -4
  292. nautobot/project-static/docs/user-guide/platform-functionality/role.html +4 -4
  293. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +4 -4
  294. nautobot/project-static/docs/user-guide/platform-functionality/status.html +4 -4
  295. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +4 -4
  296. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +4 -4
  297. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +4 -4
  298. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +4 -4
  299. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +4 -4
  300. nautobot/virtualization/tables.py +2 -5
  301. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/METADATA +3 -3
  302. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/RECORD +306 -305
  303. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/LICENSE.txt +0 -0
  304. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/NOTICE +0 -0
  305. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/WHEEL +0 -0
  306. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/entry_points.txt +0 -0
@@ -16,6 +16,7 @@ from nautobot.dcim.choices import (
16
16
  InterfaceTypeChoices,
17
17
  PortTypeChoices,
18
18
  PowerOutletFeedLegChoices,
19
+ SubdeviceRoleChoices,
19
20
  )
20
21
  from nautobot.dcim.models import (
21
22
  Cable,
@@ -886,6 +887,13 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
886
887
  self.device_type = DeviceType.objects.create(
887
888
  manufacturer=manufacturer,
888
889
  model="Test Device Type 1",
890
+ subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
891
+ )
892
+ self.child_devicetype = DeviceType.objects.create(
893
+ model="Child Device Type 1",
894
+ manufacturer=manufacturer,
895
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
896
+ u_height=0,
889
897
  )
890
898
  self.device_role = Role.objects.get_for_model(Device).first()
891
899
  self.device_status = Status.objects.get_for_model(Device).first()
@@ -1188,6 +1196,104 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1188
1196
  self.device_type.software_image_files.add(software_image_file)
1189
1197
  self.device.validated_save()
1190
1198
 
1199
+ def test_child_devices_are_not_saved_when_unnecessary(self):
1200
+ parent_device = Device.objects.create(
1201
+ name="Parent Device 1",
1202
+ location=self.location_3,
1203
+ device_type=self.device_type,
1204
+ role=self.device_role,
1205
+ status=self.device_status,
1206
+ )
1207
+ parent_device.validated_save()
1208
+
1209
+ child_device = Device.objects.create(
1210
+ name="Child Device 1",
1211
+ location=parent_device.location,
1212
+ device_type=self.child_devicetype,
1213
+ role=parent_device.role,
1214
+ status=self.device_status,
1215
+ )
1216
+ child_device.validated_save()
1217
+ child_mtime_before_parent_saved = str(child_device.last_updated)
1218
+
1219
+ devicebay = DeviceBay.objects.get(device=parent_device, name="Device Bay 1")
1220
+ devicebay.installed_device = child_device
1221
+ devicebay.validated_save()
1222
+
1223
+ #
1224
+ # Tests
1225
+ #
1226
+
1227
+ #
1228
+ # On a NOOP save, the child device shouldn't be updated
1229
+ parent_device.save()
1230
+
1231
+ child_mtime_after_parent_noop_save = str(Device.objects.get(name="Child Device 1").last_updated)
1232
+
1233
+ self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_noop_save)
1234
+
1235
+ #
1236
+ # On a serial number update, the child device shouldn't be updated
1237
+ parent_device.serial = "12345"
1238
+ parent_device.save()
1239
+
1240
+ child_mtime_after_parent_serial_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1241
+
1242
+ self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_serial_update_save)
1243
+
1244
+ #
1245
+ # If the parent rack updates, the child mtime should update.
1246
+ rack = Rack.objects.create(name="Rack 1", location=parent_device.location, status=self.device_status)
1247
+ parent_device.rack = rack
1248
+ parent_device.save()
1249
+
1250
+ child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1251
+
1252
+ self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
1253
+
1254
+ #
1255
+ # If the parent site updates, the child mtime should update
1256
+ location = Location.objects.create(
1257
+ name="New Site 1", status=self.device_status, location_type=self.location_type_3
1258
+ )
1259
+ parent_device.location = location
1260
+ parent_device.save()
1261
+
1262
+ child_mtime_after_parent_site_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1263
+
1264
+ self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
1265
+
1266
+
1267
+ class DeviceBayTestCase(ModelTestCases.BaseModelTestCase):
1268
+ model = DeviceBay
1269
+
1270
+ def setUp(self):
1271
+ self.devices = Device.objects.filter(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
1272
+ devicetype = DeviceType.objects.create(
1273
+ manufacturer=self.devices[0].device_type.manufacturer,
1274
+ model="TestDeviceType1",
1275
+ u_height=0,
1276
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
1277
+ )
1278
+ child_device = Device.objects.create(
1279
+ device_type=devicetype,
1280
+ role=self.devices[0].role,
1281
+ name="TestDevice1",
1282
+ status=self.devices[0].status,
1283
+ location=self.devices[0].location,
1284
+ )
1285
+ DeviceBay.objects.create(device=self.devices[0], name="Device Bay 1", installed_device=child_device)
1286
+
1287
+ def test_assigning_installed_device(self):
1288
+ server = Device.objects.exclude(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD).last()
1289
+ bay = DeviceBay(device=self.devices[1], name="Device Bay Err", installed_device=server)
1290
+ with self.assertRaises(ValidationError) as err:
1291
+ bay.validated_save()
1292
+ self.assertIn(
1293
+ f'Cannot install device "{server}"; device-type "{server.device_type}" subdevice_role is not "child".',
1294
+ str(err.exception),
1295
+ )
1296
+
1191
1297
 
1192
1298
  class DeviceTypeToSoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
1193
1299
  model = DeviceTypeToSoftwareImageFile
@@ -1789,6 +1789,19 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1789
1789
  sorted(interface_ips),
1790
1790
  )
1791
1791
 
1792
+ with self.subTest("Assert Assigning IPAddress Without Selecting Any IPAddress Raises Exception"):
1793
+ assign_ip_form_data["pk"] = []
1794
+ assign_ip_request = {
1795
+ "path": reverse("ipam:ipaddress_assign")
1796
+ + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
1797
+ "data": post_data(assign_ip_form_data),
1798
+ }
1799
+ response = self.client.post(**assign_ip_request, follow=True)
1800
+ self.assertHttpStatus(response, 200)
1801
+ self.assertIn(
1802
+ "Please select at least one IP Address from the table.", response.content.decode(response.charset)
1803
+ )
1804
+
1792
1805
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1793
1806
  def test_device_rearports(self):
1794
1807
  device = Device.objects.first()
nautobot/dcim/views.py CHANGED
@@ -3029,8 +3029,11 @@ class DeviceRedundancyGroupUIViewSet(NautobotUIViewSet):
3029
3029
  form_class = forms.DeviceRedundancyGroupForm
3030
3030
  queryset = (
3031
3031
  DeviceRedundancyGroup.objects.select_related("status")
3032
- .prefetch_related("devices")
3033
- .annotate(device_count=count_related(Device, "device_redundancy_group"))
3032
+ .prefetch_related("controllers", "devices")
3033
+ .annotate(
3034
+ device_count=count_related(Device, "device_redundancy_group"),
3035
+ controller_count=count_related(Controller, "controller_device_redundancy_group"),
3036
+ )
3034
3037
  )
3035
3038
  serializer_class = serializers.DeviceRedundancyGroupSerializer
3036
3039
  table_class = tables.DeviceRedundancyGroupTable
@@ -3043,6 +3046,9 @@ class DeviceRedundancyGroupUIViewSet(NautobotUIViewSet):
3043
3046
  devices_table = tables.DeviceTable(devices)
3044
3047
  devices_table.columns.show("device_redundancy_group_priority")
3045
3048
  context["devices_table"] = devices_table
3049
+ controllers = instance.controllers_sorted.restrict(request.user)
3050
+ controllers_table = tables.ControllerTable(controllers)
3051
+ context["controllers_table"] = controllers_table
3046
3052
  return context
3047
3053
 
3048
3054
 
@@ -1,5 +1,3 @@
1
- from datetime import timedelta
2
-
3
1
  from django.conf import settings
4
2
  from django.contrib.contenttypes.models import ContentType
5
3
  from django.forms import ValidationError as FormsValidationError
@@ -449,59 +447,6 @@ class ImageAttachmentViewSet(ModelViewSet):
449
447
  #
450
448
 
451
449
 
452
- def _create_schedule(serializer, data, job_model, user, approval_required, task_queue=None):
453
- """
454
- This is an internal function to create a scheduled job from API data.
455
- It has to handle both once-offs (i.e. of type TYPE_FUTURE) and interval
456
- jobs.
457
- """
458
- type_ = serializer["interval"]
459
- if type_ == JobExecutionType.TYPE_IMMEDIATELY:
460
- time = timezone.now()
461
- name = serializer.get("name") or f"{job_model.name} - {time}"
462
- elif type_ == JobExecutionType.TYPE_CUSTOM:
463
- time = serializer.get("start_time") # doing .get("key", "default") returns None instead of "default"
464
- if time is None:
465
- # "start_time" is checked against models.ScheduledJob.earliest_possible_time()
466
- # which returns timezone.now() + timedelta(seconds=15)
467
- time = timezone.now() + timedelta(seconds=20)
468
- name = serializer["name"]
469
- else:
470
- time = serializer["start_time"]
471
- name = serializer["name"]
472
- crontab = serializer.get("crontab", "")
473
-
474
- celery_kwargs = {
475
- "nautobot_job_profile": False,
476
- "queue": task_queue,
477
- }
478
-
479
- # 2.0 TODO: To revisit this as part of a larger Jobs cleanup in 2.0.
480
- #
481
- # We pass in task and job_model here partly for forward/backward compatibility logic, and
482
- # part fallback safety. It's mildly useful to store both the task module/class name and the JobModel
483
- # FK on the ScheduledJob, as in the case where the JobModel gets deleted (and the FK becomes
484
- # null) you still have a bit of context on the ScheduledJob as to what it was originally
485
- # scheduled for.
486
- scheduled_job = ScheduledJob(
487
- name=name,
488
- task=job_model.class_path,
489
- job_model=job_model,
490
- start_time=time,
491
- description=f"Nautobot job {name} scheduled by {user} for {time}",
492
- kwargs=data,
493
- celery_kwargs=celery_kwargs,
494
- interval=type_,
495
- one_off=(type_ == JobExecutionType.TYPE_FUTURE),
496
- user=user,
497
- approval_required=approval_required,
498
- crontab=crontab,
499
- queue=task_queue,
500
- )
501
- scheduled_job.validated_save()
502
- return scheduled_job
503
-
504
-
505
450
  class JobViewSetBase(
506
451
  NautobotAPIVersionMixin,
507
452
  # note no CreateModelMixin
@@ -701,13 +646,16 @@ class JobViewSetBase(
701
646
 
702
647
  # Try to create a ScheduledJob, or...
703
648
  if schedule_data:
704
- schedule = _create_schedule(
705
- schedule_data,
706
- job_class.serialize_data(cleaned_data),
649
+ schedule = ScheduledJob.create_schedule(
707
650
  job_model,
708
651
  request.user,
709
- approval_required,
652
+ name=schedule_data.get("name"),
653
+ start_time=schedule_data.get("start_time"),
654
+ interval=schedule_data.get("interval"),
655
+ crontab=schedule_data.get("crontab", ""),
656
+ approval_required=approval_required,
710
657
  task_queue=input_serializer.validated_data.get("task_queue", None),
658
+ **job_class.serialize_data(cleaned_data),
711
659
  )
712
660
  else:
713
661
  schedule = None
@@ -7,14 +7,24 @@ def get_job_results(request):
7
7
  """Callback function to collect job history for panel."""
8
8
  return (
9
9
  JobResult.objects.filter(status__in=JobResultStatusChoices.READY_STATES)
10
- .defer("result")
10
+ .restrict(request.user, "view")
11
+ .only("id", "name", "status", "date_done", "user")
11
12
  .order_by("-date_done")[:10]
12
13
  )
13
14
 
14
15
 
15
16
  def get_changelog(request):
16
17
  """Callback function to collect changelog for panel."""
17
- return ObjectChange.objects.restrict(request.user, "view")[:15]
18
+ return ObjectChange.objects.restrict(request.user, "view").only(
19
+ "id",
20
+ "action",
21
+ "changed_object",
22
+ "changed_object_id",
23
+ "changed_object_type",
24
+ "object_repr",
25
+ "user_name",
26
+ "time",
27
+ )[:15]
18
28
 
19
29
 
20
30
  layout = (
nautobot/extras/jobs.py CHANGED
@@ -20,7 +20,7 @@ from django.conf import settings
20
20
  from django.contrib.auth import get_user_model
21
21
  from django.core.exceptions import ObjectDoesNotExist
22
22
  from django.core.files.base import ContentFile
23
- from django.core.files.uploadedfile import InMemoryUploadedFile
23
+ from django.core.files.uploadedfile import UploadedFile
24
24
  from django.core.validators import RegexValidator
25
25
  from django.db.models import Model
26
26
  from django.db.models.query import QuerySet
@@ -539,7 +539,7 @@ class BaseJob:
539
539
  elif isinstance(value, Model):
540
540
  return_data[field_name] = value.pk
541
541
  # FileVar (Save each FileVar as a FileProxy)
542
- elif isinstance(value, InMemoryUploadedFile):
542
+ elif isinstance(value, UploadedFile):
543
543
  return_data[field_name] = BaseJob._save_file_to_proxy(value)
544
544
  # IPAddressVar, IPAddressWithMaskVar, IPNetworkVar
545
545
  elif isinstance(value, netaddr.ip.BaseIP):
@@ -1074,6 +1074,87 @@ class ScheduledJob(BaseModel):
1074
1074
  day_of_week=day_of_week,
1075
1075
  )
1076
1076
 
1077
+ @classmethod
1078
+ def create_schedule(
1079
+ cls,
1080
+ job_model,
1081
+ user,
1082
+ name=None,
1083
+ start_time=None,
1084
+ interval=JobExecutionType.TYPE_IMMEDIATELY,
1085
+ crontab="",
1086
+ profile=False,
1087
+ approval_required=False,
1088
+ task_queue=None,
1089
+ **job_kwargs,
1090
+ ):
1091
+ """
1092
+ Schedule a job with the specified parameters.
1093
+
1094
+ This method creates a schedule for a job to be executed at a specific time
1095
+ or interval. It handles immediate execution, custom start times, and
1096
+ crontab-based scheduling.
1097
+
1098
+ Parameters:
1099
+ job_model (JobModel): The job model instance.
1100
+ user (User): The user who is scheduling the job.
1101
+ name (str, optional): The name of the scheduled job. Defaults to None.
1102
+ start_time (datetime, optional): The start time for the job. Defaults to None.
1103
+ interval (JobExecutionType, optional): The interval type for the job execution.
1104
+ Defaults to JobExecutionType.TYPE_IMMEDIATELY.
1105
+ crontab (str, optional): The crontab string for the schedule. Defaults to "".
1106
+ profile (bool, optional): Flag indicating whether to profile the job. Defaults to False.
1107
+ approval_required (bool, optional): Flag indicating if approval is required. Defaults to False.
1108
+ task_queue (str, optional): The task queue for the job. Defaults to None, which will use the configured default celery queue.
1109
+ **job_kwargs: Additional keyword arguments to pass to the job.
1110
+
1111
+ Returns:
1112
+ ScheduledJob instance
1113
+ """
1114
+
1115
+ if interval == JobExecutionType.TYPE_IMMEDIATELY:
1116
+ start_time = timezone.now()
1117
+ name = name or f"{job_model.name} - {start_time}"
1118
+ elif interval == JobExecutionType.TYPE_CUSTOM:
1119
+ if start_time is None:
1120
+ # "start_time" is checked against models.ScheduledJob.earliest_possible_time()
1121
+ # which returns timezone.now() + timedelta(seconds=15)
1122
+ start_time = timezone.now() + timedelta(seconds=20)
1123
+
1124
+ celery_kwargs = {
1125
+ "nautobot_job_profile": profile,
1126
+ "queue": task_queue,
1127
+ }
1128
+ if job_model.soft_time_limit > 0:
1129
+ celery_kwargs["soft_time_limit"] = job_model.soft_time_limit
1130
+ if job_model.time_limit > 0:
1131
+ celery_kwargs["time_limit"] = job_model.time_limit
1132
+
1133
+ # 2.0 TODO: To revisit this as part of a larger Jobs cleanup in 2.0.
1134
+ #
1135
+ # We pass in task and job_model here partly for forward/backward compatibility logic, and
1136
+ # part fallback safety. It's mildly useful to store both the task module/class name and the JobModel
1137
+ # FK on the ScheduledJob, as in the case where the JobModel gets deleted (and the FK becomes
1138
+ # null) you still have a bit of context on the ScheduledJob as to what it was originally
1139
+ # scheduled for.
1140
+ scheduled_job = cls(
1141
+ name=name,
1142
+ task=job_model.class_path,
1143
+ job_model=job_model,
1144
+ start_time=start_time,
1145
+ description=f"Nautobot job {name} scheduled by {user} for {start_time}",
1146
+ kwargs=job_kwargs,
1147
+ celery_kwargs=celery_kwargs,
1148
+ interval=interval,
1149
+ one_off=(interval == JobExecutionType.TYPE_FUTURE),
1150
+ user=user,
1151
+ approval_required=approval_required,
1152
+ crontab=crontab,
1153
+ queue=task_queue,
1154
+ )
1155
+ scheduled_job.validated_save()
1156
+ return scheduled_job
1157
+
1077
1158
  def to_cron(self):
1078
1159
  t = self.start_time
1079
1160
  if self.interval == JobExecutionType.TYPE_HOURLY:
@@ -244,6 +244,18 @@ class RelationshipModel(models.Model):
244
244
  if relation.skip_required(cls, opposite_side):
245
245
  continue
246
246
 
247
+ if getattr(relation, f"{relation.required_on}_filter") and instance:
248
+ filterset = get_filterset_for_model(cls)
249
+ if filterset:
250
+ filter_params = getattr(relation, f"{relation.required_on}_filter")
251
+ # If the relationship is required on the model, but the object is not in the filter,
252
+ # we should allow the object to be saved, as the object is not part of the relationship.
253
+ # Example: We want a Device with a Role of Switch to be required to have a relationship
254
+ # with a Device that has a Role of Router. A Device with a Role of Printer should
255
+ # be exempt from the requirement.
256
+ if not filterset(filter_params, cls.objects.filter(id=instance.id)).qs.exists():
257
+ continue
258
+
247
259
  if relation.has_many(opposite_side):
248
260
  num_required_verbose = "at least one"
249
261
  else:
@@ -15,7 +15,7 @@ from django.core.cache import cache
15
15
  from django.core.exceptions import ValidationError
16
16
  from django.core.files.storage import get_storage_class
17
17
  from django.db import transaction
18
- from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete, pre_save
18
+ from django.db.models.signals import m2m_changed, post_delete, post_migrate, post_save, pre_delete, pre_save
19
19
  from django.dispatch import receiver
20
20
  from django.utils import timezone
21
21
  from django_prometheus.models import model_deletes, model_inserts, model_updates
@@ -268,6 +268,19 @@ def _handle_deleted_object(sender, instance, **kwargs):
268
268
  model_deletes.labels(instance._meta.model_name).inc()
269
269
 
270
270
 
271
+ #
272
+ # Content types
273
+ #
274
+
275
+
276
+ @receiver(post_migrate)
277
+ def post_migrate_clear_content_type_caches(sender, app_config, signal, **kwargs):
278
+ """Clear various content-type caches after a migration."""
279
+ with contextlib.suppress(redis.exceptions.ConnectionError):
280
+ cache.delete("nautobot.extras.utils.change_logged_models_queryset")
281
+ cache.delete_pattern("nautobot.extras.utils.FeatureQuery.*")
282
+
283
+
271
284
  #
272
285
  # Custom fields
273
286
  #
nautobot/extras/tables.py CHANGED
@@ -99,6 +99,7 @@ GITREPOSITORY_BUTTONS = """
99
99
 
100
100
  JOB_BUTTONS = """
101
101
  <a href="{% url 'extras:job' pk=record.pk %}" class="btn btn-default btn-xs" title="Details"><i class="mdi mdi-information-outline" aria-hidden="true"></i></a>
102
+ <a href="{% url 'extras:jobresult_list' %}?job_model={{ record.name | urlencode }}" class="btn btn-default btn-xs" title="Job Results"><i class="mdi mdi-format-list-bulleted" aria-hidden="true"></i></a>
102
103
  """
103
104
 
104
105
  OBJECTCHANGE_OBJECT = """
@@ -639,7 +640,10 @@ class JobTable(BaseTable):
639
640
  return render_markdown(value)
640
641
 
641
642
  def render_name(self, value):
642
- return format_html('<span class="btn btn-primary btn-xs"><i class="mdi mdi-play"></i></span>{}', value)
643
+ return format_html(
644
+ '<span class="btn btn-primary btn-xs"><i class="mdi mdi-play"></i></span>{}',
645
+ value,
646
+ )
643
647
 
644
648
  class Meta(BaseTable.Meta):
645
649
  model = JobModel
@@ -811,7 +815,16 @@ class JobResultTable(BaseTable):
811
815
  "summary",
812
816
  "actions",
813
817
  )
814
- default_columns = ("pk", "date_created", "name", "job_model", "user", "status", "summary", "actions")
818
+ default_columns = (
819
+ "pk",
820
+ "date_created",
821
+ "name",
822
+ "job_model",
823
+ "user",
824
+ "status",
825
+ "summary",
826
+ "actions",
827
+ )
815
828
 
816
829
 
817
830
  class JobButtonTable(BaseTable):
@@ -953,7 +966,15 @@ class RelationshipAssociationTable(BaseTable):
953
966
 
954
967
  class Meta(BaseTable.Meta):
955
968
  model = RelationshipAssociation
956
- fields = ("pk", "relationship", "source_type", "source", "destination_type", "destination", "actions")
969
+ fields = (
970
+ "pk",
971
+ "relationship",
972
+ "source_type",
973
+ "source",
974
+ "destination_type",
975
+ "destination",
976
+ "actions",
977
+ )
957
978
  default_columns = ("pk", "relationship", "source", "destination", "actions")
958
979
 
959
980
 
@@ -1069,7 +1090,15 @@ class TagTable(BaseTable):
1069
1090
 
1070
1091
  class Meta(BaseTable.Meta):
1071
1092
  model = Tag
1072
- fields = ("pk", "name", "items", "color", "content_types", "description", "actions")
1093
+ fields = (
1094
+ "pk",
1095
+ "name",
1096
+ "items",
1097
+ "color",
1098
+ "content_types",
1099
+ "description",
1100
+ "actions",
1101
+ )
1073
1102
 
1074
1103
 
1075
1104
  class TaggedItemTable(BaseTable):
@@ -1149,7 +1178,9 @@ class WebhookTable(BaseTable):
1149
1178
  class AssociatedContactsTable(StatusTableMixin, RoleTableMixin, BaseTable):
1150
1179
  pk = ToggleColumn()
1151
1180
  contact_type = tables.TemplateColumn(
1152
- CONTACT_OR_TEAM_ICON, verbose_name="Type", attrs={"td": {"style": "width:20px;"}}
1181
+ CONTACT_OR_TEAM_ICON,
1182
+ verbose_name="Type",
1183
+ attrs={"td": {"style": "width:20px;"}},
1153
1184
  )
1154
1185
  name = tables.TemplateColumn(CONTACT_OR_TEAM, verbose_name="Name")
1155
1186
  contact_or_team_phone = tables.TemplateColumn(PHONE, accessor="contact_or_team.phone", verbose_name="Phone")
@@ -115,6 +115,17 @@
115
115
  <td>{{ object.enabled | render_boolean }}</td>
116
116
  <td></td>
117
117
  </tr>
118
+ <tr>
119
+ <td>Job Results</td>
120
+ <td>
121
+ {% if object.job_results.exists %}
122
+ <a href="{% url 'extras:jobresult_list' %}?job_model={{ object.name | urlencode }}">{{ object.job_results.count }}</a>
123
+ {% else %}
124
+ {{ None|placeholder }}
125
+ {% endif %}
126
+ </td>
127
+ <td></td>
128
+ </tr>
118
129
  </table>
119
130
  </div>
120
131
  {% endblock content_left_page %}
@@ -74,7 +74,7 @@ class DynamicGroupTestCase(SeleniumTestCase):
74
74
 
75
75
  # And just a cursory check to make sure that the filter worked.
76
76
  group = DynamicGroup.objects.get(name=name)
77
- self.assertEqual(group.count, len(devices))
77
+ self.assertEqual(group.count, Device.objects.filter(status__name="Active").count())
78
78
  self.assertEqual(group.filter, {"status": ["Active"]})
79
79
 
80
80
  # Verify dynamic group shows up on device detail tab