nautobot 2.1.7__py3-none-any.whl → 2.1.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 (338) hide show
  1. nautobot/apps/api.py +1 -2
  2. nautobot/apps/utils.py +4 -0
  3. nautobot/apps/views.py +2 -0
  4. nautobot/circuits/api/urls.py +1 -2
  5. nautobot/circuits/api/views.py +0 -12
  6. nautobot/circuits/tests/integration/test_relationships.py +0 -4
  7. nautobot/core/api/routers.py +25 -3
  8. nautobot/core/api/utils.py +4 -0
  9. nautobot/core/api/views.py +21 -15
  10. nautobot/core/celery/schedulers.py +13 -0
  11. nautobot/core/choices.py +0 -21
  12. nautobot/core/models/__init__.py +1 -1
  13. nautobot/core/models/tree_queries.py +29 -7
  14. nautobot/core/releases.py +1 -1
  15. nautobot/core/settings.py +9 -0
  16. nautobot/core/settings_funcs.py +0 -18
  17. nautobot/core/signals.py +5 -5
  18. nautobot/core/tasks.py +7 -3
  19. nautobot/core/templates/admin/base.html +23 -94
  20. nautobot/core/templates/generic/object_list.html +2 -0
  21. nautobot/core/templates/graphene/graphiql.html +18 -47
  22. nautobot/core/templates/inc/footer.html +5 -5
  23. nautobot/core/templates/inc/nav_menu.html +0 -7
  24. nautobot/core/templates/nautobot_config.py.j2 +6 -0
  25. nautobot/core/templates/rest_framework/api.html +12 -5
  26. nautobot/core/testing/mixins.py +13 -5
  27. nautobot/core/tests/integration/test_plugin_navbar.py +7 -21
  28. nautobot/core/tests/integration/test_view_authentication.py +67 -0
  29. nautobot/core/tests/runner.py +25 -2
  30. nautobot/core/tests/test_graphql.py +2 -14
  31. nautobot/core/tests/test_models.py +3 -3
  32. nautobot/core/tests/test_navigations.py +67 -10
  33. nautobot/core/tests/test_releases.py +9 -3
  34. nautobot/core/tests/test_views.py +23 -16
  35. nautobot/core/utils/lookup.py +124 -0
  36. nautobot/core/views/__init__.py +3 -7
  37. nautobot/core/views/generic.py +9 -0
  38. nautobot/dcim/api/urls.py +1 -2
  39. nautobot/dcim/api/views.py +1 -12
  40. nautobot/dcim/choices.py +56 -0
  41. nautobot/dcim/models/racks.py +1 -3
  42. nautobot/dcim/navigation.py +1 -1
  43. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +67 -43
  44. nautobot/dcim/tests/test_api.py +3 -0
  45. nautobot/dcim/tests/test_filters.py +0 -28
  46. nautobot/dcim/views.py +5 -2
  47. nautobot/extras/api/urls.py +1 -2
  48. nautobot/extras/api/views.py +0 -10
  49. nautobot/extras/choices.py +14 -0
  50. nautobot/extras/models/customfields.py +93 -34
  51. nautobot/extras/models/groups.py +1 -1
  52. nautobot/extras/models/relationships.py +32 -19
  53. nautobot/extras/navigation.py +3 -2
  54. nautobot/extras/plugins/__init__.py +8 -0
  55. nautobot/extras/plugins/views.py +6 -9
  56. nautobot/extras/querysets.py +1 -1
  57. nautobot/extras/signals.py +12 -6
  58. nautobot/extras/templates/extras/customfield.html +22 -14
  59. nautobot/extras/templatetags/job_buttons.py +7 -0
  60. nautobot/extras/templatetags/plugins.py +5 -1
  61. nautobot/extras/tests/test_customfields.py +323 -287
  62. nautobot/extras/tests/test_dynamicgroups.py +1 -1
  63. nautobot/extras/tests/test_jobs.py +2 -2
  64. nautobot/extras/tests/test_plugins.py +41 -0
  65. nautobot/extras/tests/test_relationships.py +31 -14
  66. nautobot/extras/tests/test_views.py +124 -1
  67. nautobot/extras/utils.py +7 -3
  68. nautobot/extras/views.py +10 -10
  69. nautobot/ipam/api/urls.py +1 -2
  70. nautobot/ipam/api/views.py +6 -13
  71. nautobot/ipam/tables.py +0 -1
  72. nautobot/ipam/tests/test_graphql.py +2 -3
  73. nautobot/ipam/views.py +12 -10
  74. nautobot/project-static/css/base.css +1 -0
  75. nautobot/project-static/docs/404.html +30 -2
  76. nautobot/project-static/docs/apps/index.html +30 -2
  77. nautobot/project-static/docs/apps/nautobot-apps.html +30 -2
  78. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +30 -2
  79. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +30 -2
  80. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +410 -410
  81. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +30 -2
  82. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +386 -358
  83. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +30 -2
  84. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +30 -2
  85. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +30 -2
  86. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +30 -2
  87. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +45 -17
  88. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +30 -2
  89. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +30 -2
  90. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +30 -2
  91. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +759 -602
  92. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +30 -2
  93. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +30 -2
  94. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +30 -2
  95. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +528 -467
  96. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +205 -109
  97. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +30 -2
  98. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +1265 -785
  99. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1827 -1746
  100. nautobot/project-static/docs/development/apps/api/configuration-view.html +30 -2
  101. nautobot/project-static/docs/development/apps/api/database-backend-config.html +30 -2
  102. nautobot/project-static/docs/development/apps/api/models/django-admin.html +30 -2
  103. nautobot/project-static/docs/development/apps/api/models/global-search.html +30 -2
  104. nautobot/project-static/docs/development/apps/api/models/graphql.html +30 -2
  105. nautobot/project-static/docs/development/apps/api/models/index.html +30 -2
  106. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +31 -3
  107. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +30 -2
  108. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +30 -2
  109. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +30 -2
  110. nautobot/project-static/docs/development/apps/api/platform-features/index.html +30 -2
  111. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +30 -2
  112. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +30 -2
  113. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +30 -2
  114. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +30 -2
  115. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +30 -2
  116. nautobot/project-static/docs/development/apps/api/prometheus.html +30 -2
  117. nautobot/project-static/docs/development/apps/api/setup.html +30 -2
  118. nautobot/project-static/docs/development/apps/api/testing.html +33 -5
  119. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +30 -2
  120. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +30 -2
  121. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +30 -2
  122. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +33 -5
  123. nautobot/project-static/docs/development/apps/api/ui-extensions/object-detail-views.html +13 -5559
  124. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +5594 -0
  125. nautobot/project-static/docs/development/apps/api/ui-extensions/tabs.html +3 -3
  126. nautobot/project-static/docs/development/apps/api/views/base-template.html +30 -2
  127. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +44 -11
  128. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +47 -14
  129. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +30 -2
  130. nautobot/project-static/docs/development/apps/api/views/index.html +30 -2
  131. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +30 -2
  132. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +30 -2
  133. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +30 -2
  134. nautobot/project-static/docs/development/apps/api/views/notes.html +30 -2
  135. nautobot/project-static/docs/development/apps/api/views/rest-api.html +30 -2
  136. nautobot/project-static/docs/development/apps/api/views/urls.html +30 -2
  137. nautobot/project-static/docs/development/apps/index.html +30 -2
  138. nautobot/project-static/docs/development/apps/migration/code-updates.html +30 -2
  139. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +30 -2
  140. nautobot/project-static/docs/development/apps/migration/from-v1.html +30 -2
  141. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +30 -2
  142. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +30 -2
  143. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +30 -2
  144. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +30 -2
  145. nautobot/project-static/docs/development/apps/porting-from-netbox.html +30 -2
  146. nautobot/project-static/docs/development/core/application-registry.html +30 -2
  147. nautobot/project-static/docs/development/core/best-practices.html +33 -5
  148. nautobot/project-static/docs/development/core/bootstrap-ui.html +30 -2
  149. nautobot/project-static/docs/development/core/caching.html +5481 -0
  150. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +30 -2
  151. nautobot/project-static/docs/development/core/extending-models.html +33 -5
  152. nautobot/project-static/docs/development/core/generic-views.html +30 -2
  153. nautobot/project-static/docs/development/core/getting-started.html +49 -12
  154. nautobot/project-static/docs/development/core/homepage.html +30 -2
  155. nautobot/project-static/docs/development/core/index.html +30 -2
  156. nautobot/project-static/docs/development/core/model-features.html +30 -2
  157. nautobot/project-static/docs/development/core/natural-keys.html +30 -2
  158. nautobot/project-static/docs/development/core/navigation-menu.html +30 -2
  159. nautobot/project-static/docs/development/core/release-checklist.html +30 -2
  160. nautobot/project-static/docs/development/core/role-internals.html +30 -2
  161. nautobot/project-static/docs/development/core/style-guide.html +30 -2
  162. nautobot/project-static/docs/development/core/templates.html +30 -2
  163. nautobot/project-static/docs/development/core/testing.html +30 -2
  164. nautobot/project-static/docs/development/core/user-preferences.html +30 -2
  165. nautobot/project-static/docs/development/index.html +30 -2
  166. nautobot/project-static/docs/development/jobs/index.html +30 -2
  167. nautobot/project-static/docs/development/jobs/migration/from-v1.html +30 -2
  168. nautobot/project-static/docs/index.html +30 -2
  169. nautobot/project-static/docs/objects.inv +0 -0
  170. nautobot/project-static/docs/release-notes/index.html +30 -2
  171. nautobot/project-static/docs/release-notes/version-1.0.html +30 -2
  172. nautobot/project-static/docs/release-notes/version-1.1.html +30 -2
  173. nautobot/project-static/docs/release-notes/version-1.2.html +30 -2
  174. nautobot/project-static/docs/release-notes/version-1.3.html +30 -2
  175. nautobot/project-static/docs/release-notes/version-1.4.html +31 -3
  176. nautobot/project-static/docs/release-notes/version-1.5.html +30 -2
  177. nautobot/project-static/docs/release-notes/version-1.6.html +573 -134
  178. nautobot/project-static/docs/release-notes/version-2.0.html +30 -2
  179. nautobot/project-static/docs/release-notes/version-2.1.html +539 -170
  180. nautobot/project-static/docs/search/search_index.json +1 -1
  181. nautobot/project-static/docs/sitemap.xml +250 -240
  182. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  183. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +30 -2
  184. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +30 -2
  185. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +30 -2
  186. nautobot/project-static/docs/user-guide/administration/configuration/index.html +30 -2
  187. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +49 -2
  188. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +30 -2
  189. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +30 -2
  190. nautobot/project-static/docs/user-guide/administration/guides/caching.html +30 -2
  191. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +30 -2
  192. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +30 -2
  193. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +30 -2
  194. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +30 -2
  195. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +30 -2
  196. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +30 -2
  197. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +30 -2
  198. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +30 -2
  199. nautobot/project-static/docs/user-guide/administration/installation/docker.html +37 -5
  200. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +30 -2
  201. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +6019 -0
  202. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +30 -2
  203. nautobot/project-static/docs/user-guide/administration/installation/index.html +30 -2
  204. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +30 -2
  205. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +30 -2
  206. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +33 -5
  207. nautobot/project-static/docs/user-guide/administration/installation/services.html +30 -2
  208. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +30 -2
  209. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +30 -2
  210. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +30 -2
  211. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +30 -2
  212. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +30 -2
  213. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +30 -2
  214. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +30 -2
  215. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +30 -2
  216. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +30 -2
  217. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +30 -2
  218. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +30 -2
  219. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +30 -2
  220. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +30 -2
  221. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +30 -2
  222. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +30 -2
  223. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +30 -2
  224. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +30 -2
  225. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +30 -2
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +30 -2
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +30 -2
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +30 -2
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +30 -2
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +30 -2
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +30 -2
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +30 -2
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +30 -2
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +30 -2
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +30 -2
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +30 -2
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +30 -2
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +30 -2
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +30 -2
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +30 -2
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +30 -2
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +30 -2
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +30 -2
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +30 -2
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +30 -2
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +30 -2
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +30 -2
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +30 -2
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +30 -2
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +30 -2
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +30 -2
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +30 -2
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +30 -2
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +30 -2
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +30 -2
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +30 -2
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +30 -2
  258. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +30 -2
  259. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +30 -2
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +30 -2
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +30 -2
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +30 -2
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +30 -2
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +30 -2
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +30 -2
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +30 -2
  267. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +30 -2
  268. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +30 -2
  269. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +30 -2
  270. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +30 -2
  271. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +30 -2
  272. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +30 -2
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +30 -2
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +30 -2
  275. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +30 -2
  276. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +30 -2
  277. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +33 -5
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +30 -2
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +30 -2
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +30 -2
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +30 -2
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +30 -2
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +30 -2
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +30 -2
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +30 -2
  286. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +30 -2
  287. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +30 -2
  288. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +30 -2
  289. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +30 -2
  290. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +30 -2
  291. nautobot/project-static/docs/user-guide/index.html +30 -2
  292. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +30 -2
  293. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +30 -2
  294. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +111 -15
  295. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +30 -2
  296. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +30 -2
  297. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +30 -2
  298. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +30 -2
  299. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +30 -2
  300. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +30 -2
  301. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +30 -2
  302. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +30 -2
  303. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +30 -2
  304. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +30 -2
  305. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +30 -2
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +30 -2
  307. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +30 -2
  308. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +30 -2
  309. nautobot/project-static/docs/user-guide/platform-functionality/note.html +30 -2
  310. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +30 -2
  311. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +30 -2
  312. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +30 -2
  313. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +30 -2
  314. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +30 -2
  315. nautobot/project-static/docs/user-guide/platform-functionality/role.html +30 -2
  316. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +30 -2
  317. nautobot/project-static/docs/user-guide/platform-functionality/status.html +30 -2
  318. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +30 -2
  319. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +30 -2
  320. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +30 -2
  321. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +30 -2
  322. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +30 -2
  323. nautobot/tenancy/api/urls.py +1 -2
  324. nautobot/tenancy/api/views.py +0 -12
  325. nautobot/tenancy/navigation.py +1 -1
  326. nautobot/tenancy/tests/test_filters.py +0 -168
  327. nautobot/users/api/urls.py +1 -2
  328. nautobot/users/api/views.py +2 -65
  329. nautobot/users/views.py +8 -8
  330. nautobot/virtualization/api/urls.py +1 -2
  331. nautobot/virtualization/api/views.py +0 -12
  332. nautobot/virtualization/tests/test_filters.py +0 -28
  333. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/METADATA +2 -2
  334. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/RECORD +338 -334
  335. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/LICENSE.txt +0 -0
  336. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/NOTICE +0 -0
  337. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/WHEEL +0 -0
  338. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import logging
2
3
 
3
4
  from django.conf import settings
@@ -42,7 +43,6 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
42
43
  def test_immutable_fields(self):
43
44
  """Some fields may not be changed once set, due to the potential for complex downstream effects."""
44
45
  instance = CustomField(
45
- # 2.0 TODO: #824 remove name field
46
46
  label="Custom Field",
47
47
  key="custom_field",
48
48
  type=CustomFieldTypeChoices.TYPE_TEXT,
@@ -96,6 +96,11 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
96
96
  "field_value": "http://example.com/",
97
97
  "empty_value": "",
98
98
  },
99
+ {
100
+ "field_type": CustomFieldTypeChoices.TYPE_MARKDOWN,
101
+ "field_value": "### Hello world!\n\n- Item 1\n- Item 2\n- Item 3",
102
+ "empty_value": "",
103
+ },
99
104
  {
100
105
  "field_type": CustomFieldTypeChoices.TYPE_JSON,
101
106
  "field_value": {"dict_key": "key value"},
@@ -394,35 +399,57 @@ class CustomFieldManagerTest(TestCase):
394
399
  self.assertEqual(CustomField.objects.get_for_model(Location).count(), 2)
395
400
  self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
396
401
 
397
- def test_get_for_model_lru_cache_invalidation(self):
398
- """Test that the lru cache is properly invalidated when CustomFields are created or deleted."""
399
-
400
- qs1 = CustomField.objects.get_for_model(Location)
401
-
402
+ def test_get_for_model_caching_and_cache_invalidation(self):
403
+ """Test that the cache is used and is properly invalidated when CustomFields are created or deleted."""
402
404
  # Assert that the cache is used when calling get_for_model a second time
403
- qs1_cached = CustomField.objects.get_for_model(Location)
404
- self.assertTrue(qs1_cached is qs1)
405
+ CustomField.objects.get_for_model(Location)
406
+ with self.assertNumQueries(0):
407
+ CustomField.objects.get_for_model(Location)
408
+
409
+ # Assert that different values of exclude_filter_disabled are cached separately
410
+ with self.assertNumQueries(1):
411
+ CustomField.objects.get_for_model(Location, exclude_filter_disabled=True)
412
+ with self.assertNumQueries(0):
413
+ CustomField.objects.get_for_model(Location, exclude_filter_disabled=True)
414
+ with self.assertNumQueries(0):
415
+ CustomField.objects.get_for_model(Location)
416
+
417
+ # Assert that different models are cached separately
418
+ with self.assertNumQueries(1):
419
+ CustomField.objects.get_for_model(VirtualMachine)
420
+ with self.assertNumQueries(0):
421
+ CustomField.objects.get_for_model(VirtualMachine)
422
+ with self.assertNumQueries(0):
423
+ CustomField.objects.get_for_model(Location)
405
424
 
406
425
  # Assert that the cache is invalidated on object save
407
426
  custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, label="Test CF1", default="foo")
408
427
  custom_field.save()
409
- qs2 = CustomField.objects.get_for_model(Location)
410
- self.assertFalse(qs2 is qs1)
428
+ with self.assertNumQueries(1):
429
+ CustomField.objects.get_for_model(Location)
430
+ with self.assertNumQueries(0):
431
+ CustomField.objects.get_for_model(Location)
411
432
 
412
433
  # Assert that the cache is invalidated when adding a CustomField.content_types m2m relationship
413
434
  custom_field.content_types.set([self.content_type])
414
- qs3 = CustomField.objects.get_for_model(Location)
415
- self.assertNotIn(qs3, (qs1, qs2))
435
+ with self.assertNumQueries(1):
436
+ CustomField.objects.get_for_model(Location)
437
+ with self.assertNumQueries(0):
438
+ CustomField.objects.get_for_model(Location)
416
439
 
417
440
  # Assert that the cache is invalidated when removing a CustomField.content_types m2m relationship
418
441
  custom_field.content_types.set([])
419
- qs4 = CustomField.objects.get_for_model(Location)
420
- self.assertNotIn(qs4, (qs1, qs2, qs3))
442
+ with self.assertNumQueries(1):
443
+ CustomField.objects.get_for_model(Location)
444
+ with self.assertNumQueries(0):
445
+ CustomField.objects.get_for_model(Location)
421
446
 
422
447
  # Assert that the cache is invalidated on object delete
423
448
  custom_field.delete()
424
- qs5 = CustomField.objects.get_for_model(Location)
425
- self.assertNotIn(qs5, (qs1, qs2, qs3, qs4))
449
+ with self.assertNumQueries(1):
450
+ CustomField.objects.get_for_model(Location)
451
+ with self.assertNumQueries(0):
452
+ CustomField.objects.get_for_model(Location)
426
453
 
427
454
 
428
455
  class CustomFieldDataAPITest(APITestCase):
@@ -432,130 +459,162 @@ class CustomFieldDataAPITest(APITestCase):
432
459
  For tests of the api/extras/custom-fields/ REST API endpoint itself, see test_api.py.
433
460
  """
434
461
 
435
- @classmethod
436
- def setUpTestData(cls):
462
+ user_permissions = (
463
+ "dcim.add_location",
464
+ "dcim.change_location",
465
+ "dcim.view_location",
466
+ )
467
+
468
+ def setUp(self):
469
+ super().setUp()
437
470
  content_type = ContentType.objects.get_for_model(Location)
438
471
 
439
472
  # Text custom field
440
- cls.cf_text = CustomField(
473
+ self.cf_text = CustomField(
441
474
  type=CustomFieldTypeChoices.TYPE_TEXT, label="Text Field", key="text_cf", default="FOO"
442
475
  )
443
- cls.cf_text.save()
444
- cls.cf_text.content_types.set([content_type])
476
+ self.cf_text.validated_save()
477
+ self.cf_text.content_types.set([content_type])
445
478
 
446
479
  # Integer custom field
447
- cls.cf_integer = CustomField(
480
+ self.cf_integer = CustomField(
448
481
  type=CustomFieldTypeChoices.TYPE_INTEGER, label="Number Field", key="number_cf", default=12
449
482
  )
450
- cls.cf_integer.save()
451
- cls.cf_integer.content_types.set([content_type])
483
+ self.cf_integer.validated_save()
484
+ self.cf_integer.content_types.set([content_type])
452
485
 
453
486
  # Boolean custom field
454
- cls.cf_boolean = CustomField(
487
+ self.cf_boolean = CustomField(
455
488
  type=CustomFieldTypeChoices.TYPE_BOOLEAN,
456
489
  label="Boolean Field",
457
490
  key="boolean_cf",
458
491
  default=False,
459
492
  )
460
- cls.cf_boolean.save()
461
- cls.cf_boolean.content_types.set([content_type])
493
+ self.cf_boolean.validated_save()
494
+ self.cf_boolean.content_types.set([content_type])
462
495
 
463
496
  # Date custom field
464
- cls.cf_date = CustomField(
497
+ self.cf_date = CustomField(
465
498
  type=CustomFieldTypeChoices.TYPE_DATE,
466
499
  label="Date Field",
467
500
  key="date_cf",
468
501
  default="2020-01-01",
469
502
  )
470
- cls.cf_date.save()
471
- cls.cf_date.content_types.set([content_type])
503
+ self.cf_date.validated_save()
504
+ self.cf_date.content_types.set([content_type])
472
505
 
473
506
  # URL custom field
474
- cls.cf_url = CustomField(
507
+ self.cf_url = CustomField(
475
508
  type=CustomFieldTypeChoices.TYPE_URL,
476
509
  label="URL Field",
477
510
  key="url_cf",
478
511
  default="http://example.com/1",
479
512
  )
480
- cls.cf_url.save()
481
- cls.cf_url.content_types.set([content_type])
513
+ self.cf_url.validated_save()
514
+ self.cf_url.content_types.set([content_type])
482
515
 
483
516
  # Select custom field
484
- cls.cf_select = CustomField(
517
+ self.cf_select = CustomField(
485
518
  type=CustomFieldTypeChoices.TYPE_SELECT,
486
519
  label="Choice Field",
487
520
  key="choice_cf",
488
521
  )
489
- cls.cf_select.save()
490
- cls.cf_select.content_types.set([content_type])
491
- CustomFieldChoice.objects.create(custom_field=cls.cf_select, value="Foo")
492
- CustomFieldChoice.objects.create(custom_field=cls.cf_select, value="Bar")
493
- CustomFieldChoice.objects.create(custom_field=cls.cf_select, value="Baz")
494
- cls.cf_select.default = "Foo"
495
- cls.cf_select.save()
522
+ self.cf_select.validated_save()
523
+ self.cf_select.content_types.set([content_type])
524
+ CustomFieldChoice.objects.create(custom_field=self.cf_select, value="Foo")
525
+ CustomFieldChoice.objects.create(custom_field=self.cf_select, value="Bar")
526
+ CustomFieldChoice.objects.create(custom_field=self.cf_select, value="Baz")
527
+ self.cf_select.default = "Foo"
528
+ self.cf_select.validated_save()
496
529
 
497
530
  # Multi-select custom field
498
- cls.cf_multi_select = CustomField(
531
+ self.cf_multi_select = CustomField(
499
532
  type=CustomFieldTypeChoices.TYPE_MULTISELECT,
500
533
  label="Multiple Choice Field",
501
534
  key="multi_choice_cf",
502
535
  )
503
- cls.cf_multi_select.save()
504
- cls.cf_multi_select.content_types.set([content_type])
505
- CustomFieldChoice.objects.create(custom_field=cls.cf_multi_select, value="Foo")
506
- CustomFieldChoice.objects.create(custom_field=cls.cf_multi_select, value="Bar")
507
- CustomFieldChoice.objects.create(custom_field=cls.cf_multi_select, value="Baz")
508
- cls.cf_multi_select.default = ["Foo", "Bar"]
509
- cls.cf_multi_select.save()
536
+ self.cf_multi_select.validated_save()
537
+ self.cf_multi_select.content_types.set([content_type])
538
+ CustomFieldChoice.objects.create(custom_field=self.cf_multi_select, value="Foo")
539
+ CustomFieldChoice.objects.create(custom_field=self.cf_multi_select, value="Bar")
540
+ CustomFieldChoice.objects.create(custom_field=self.cf_multi_select, value="Baz")
541
+ self.cf_multi_select.default = ["Foo", "Bar"]
542
+ self.cf_multi_select.validated_save()
543
+
544
+ # Markdown custom field
545
+ self.cf_markdown = CustomField(
546
+ type=CustomFieldTypeChoices.TYPE_MARKDOWN,
547
+ label="Markdown Field",
548
+ key="markdown_cf",
549
+ default="# One\n\n## Two\n\n### Three",
550
+ )
551
+ self.cf_markdown.validated_save()
552
+ self.cf_markdown.content_types.set([content_type])
553
+
554
+ # JSON custom field
555
+ self.cf_json = CustomField(
556
+ type=CustomFieldTypeChoices.TYPE_JSON,
557
+ label="JSON Field",
558
+ key="json_cf",
559
+ default={"dict": ["key1", "key2"]},
560
+ )
561
+ self.cf_json.validated_save()
562
+ self.cf_json.content_types.set([content_type])
563
+
564
+ self.all_cfs = [
565
+ self.cf_text,
566
+ self.cf_integer,
567
+ self.cf_boolean,
568
+ self.cf_date,
569
+ self.cf_url,
570
+ self.cf_select,
571
+ self.cf_multi_select,
572
+ self.cf_markdown,
573
+ self.cf_json,
574
+ ]
510
575
 
511
576
  if "example_plugin" in settings.PLUGINS:
512
- cls.cf_plugin_field = CustomField.objects.get(key="example_plugin_auto_custom_field")
577
+ self.cf_plugin_field = CustomField.objects.get(key="example_plugin_auto_custom_field")
578
+ self.all_cfs.append(self.cf_plugin_field)
513
579
 
514
- cls.statuses = Status.objects.get_for_model(Location)
580
+ self.statuses = Status.objects.get_for_model(Location)
515
581
 
516
582
  # Create some locations
517
- cls.lt = LocationType.objects.get(name="Campus")
518
- cls.locations = (
519
- Location.objects.create(name="Location 1", status=cls.statuses[0], location_type=cls.lt),
520
- Location.objects.create(name="Location 2", status=cls.statuses[0], location_type=cls.lt),
583
+ self.lt = LocationType.objects.get(name="Campus")
584
+ self.locations = (
585
+ Location.objects.create(name="Location 1", status=self.statuses[0], location_type=self.lt),
586
+ Location.objects.create(name="Location 2", status=self.statuses[0], location_type=self.lt),
521
587
  )
522
588
 
523
589
  # Assign custom field values for location 2
524
- cls.locations[1]._custom_field_data = {
525
- cls.cf_text.key: "bar",
526
- cls.cf_integer.key: 456,
527
- cls.cf_boolean.key: True,
528
- cls.cf_date.key: "2020-01-02",
529
- cls.cf_url.key: "http://example.com/2",
530
- cls.cf_select.key: "Bar",
531
- cls.cf_multi_select.key: ["Bar", "Baz"],
590
+ self.locations[1]._custom_field_data = {
591
+ self.cf_text.key: "bar",
592
+ self.cf_integer.key: 456,
593
+ self.cf_boolean.key: True,
594
+ self.cf_date.key: "2020-01-02",
595
+ self.cf_url.key: "http://example.com/2",
596
+ self.cf_select.key: "Bar",
597
+ self.cf_multi_select.key: ["Bar", "Baz"],
598
+ self.cf_markdown.key: "### Hello world!\n\n- Item 1\n- Item 2\n- Item 3",
599
+ self.cf_json.key: {"hello": "world"},
532
600
  }
533
601
  if "example_plugin" in settings.PLUGINS:
534
- cls.locations[1]._custom_field_data[cls.cf_plugin_field.key] = "Custom value"
535
- cls.locations[1].save()
602
+ self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
603
+ self.locations[1].validated_save()
604
+ self.list_url = reverse("dcim-api:location-list")
605
+ self.detail_url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
536
606
 
537
607
  def test_get_single_object_without_custom_field_data(self):
538
608
  """
539
609
  Validate that custom fields are present on an object even if it has no values defined.
540
610
  """
541
611
  url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[0].pk})
542
- self.add_permissions("dcim.view_location")
543
612
 
544
613
  response = self.client.get(url, **self.header)
545
614
  self.assertEqual(response.data["name"], self.locations[0].name)
546
615
  # A model directly instantiated via the ORM does NOT automatically receive custom field default values.
547
616
  # This is arguably a bug. See https://github.com/nautobot/nautobot/issues/3312 for details.
548
- expected_data = {
549
- "text_cf": None,
550
- "number_cf": None,
551
- "boolean_cf": None,
552
- "date_cf": None,
553
- "url_cf": None,
554
- "choice_cf": None,
555
- "multi_choice_cf": None,
556
- }
557
- if "example_plugin" in settings.PLUGINS:
558
- expected_data["example_plugin_auto_custom_field"] = None
617
+ expected_data = {cf.key: None for cf in self.all_cfs}
559
618
  self.assertEqual(response.data["custom_fields"], expected_data)
560
619
 
561
620
  def test_get_single_object_with_custom_field_data(self):
@@ -563,18 +622,12 @@ class CustomFieldDataAPITest(APITestCase):
563
622
  Validate that custom fields are present and correctly set for an object with values defined.
564
623
  """
565
624
  location2_cfvs = self.locations[1].cf
566
- url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
567
- self.add_permissions("dcim.view_location")
568
-
569
- response = self.client.get(url, **self.header)
625
+ response = self.client.get(self.detail_url, **self.header)
570
626
  self.assertEqual(response.data["name"], self.locations[1].name)
571
- self.assertEqual(response.data["custom_fields"]["text_cf"], location2_cfvs["text_cf"])
572
- self.assertEqual(response.data["custom_fields"]["number_cf"], location2_cfvs["number_cf"])
573
- self.assertEqual(response.data["custom_fields"]["boolean_cf"], location2_cfvs["boolean_cf"])
574
- self.assertEqual(response.data["custom_fields"]["date_cf"], location2_cfvs["date_cf"])
575
- self.assertEqual(response.data["custom_fields"]["url_cf"], location2_cfvs["url_cf"])
576
- self.assertEqual(response.data["custom_fields"]["choice_cf"], location2_cfvs["choice_cf"])
577
- self.assertEqual(response.data["custom_fields"]["multi_choice_cf"], location2_cfvs["multi_choice_cf"])
627
+ for cf in self.all_cfs:
628
+ self.assertIn(cf.key, response.data["custom_fields"])
629
+ self.assertIn(cf.key, location2_cfvs)
630
+ self.assertEqual(response.data["custom_fields"][cf.key], location2_cfvs[cf.key])
578
631
 
579
632
  def test_create_single_object_with_defaults(self):
580
633
  """
@@ -585,35 +638,20 @@ class CustomFieldDataAPITest(APITestCase):
585
638
  "location_type": self.lt.pk,
586
639
  "status": self.statuses[0].pk,
587
640
  }
588
- url = reverse("dcim-api:location-list")
589
- self.add_permissions("dcim.add_location")
590
-
591
- response = self.client.post(url, data, format="json", **self.header)
641
+ response = self.client.post(self.list_url, data, format="json", **self.header)
592
642
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
593
643
 
594
644
  # Validate response data
595
645
  response_cf = response.data["custom_fields"]
596
- self.assertEqual(response_cf["text_cf"], self.cf_text.default)
597
- self.assertEqual(response_cf["number_cf"], self.cf_integer.default)
598
- self.assertEqual(response_cf["boolean_cf"], self.cf_boolean.default)
599
- self.assertEqual(response_cf["date_cf"], self.cf_date.default)
600
- self.assertEqual(response_cf["url_cf"], self.cf_url.default)
601
- self.assertEqual(response_cf["choice_cf"], self.cf_select.default)
602
- self.assertEqual(response_cf["multi_choice_cf"], self.cf_multi_select.default)
603
- if "example_plugin" in settings.PLUGINS:
604
- self.assertEqual(response_cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
646
+ for cf in self.all_cfs:
647
+ self.assertIn(cf.key, response_cf)
648
+ self.assertEqual(response_cf[cf.key], cf.default)
605
649
 
606
650
  # Validate database data
607
651
  location = Location.objects.get(pk=response.data["id"])
608
- self.assertEqual(location.cf["text_cf"], self.cf_text.default)
609
- self.assertEqual(location.cf["number_cf"], self.cf_integer.default)
610
- self.assertEqual(location.cf["boolean_cf"], self.cf_boolean.default)
611
- self.assertEqual(str(location.cf["date_cf"]), self.cf_date.default)
612
- self.assertEqual(location.cf["url_cf"], self.cf_url.default)
613
- self.assertEqual(location.cf["choice_cf"], self.cf_select.default)
614
- self.assertEqual(location.cf["multi_choice_cf"], self.cf_multi_select.default)
615
- if "example_plugin" in settings.PLUGINS:
616
- self.assertEqual(location.cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
652
+ for cf in self.all_cfs:
653
+ self.assertIn(cf.key, location.cf)
654
+ self.assertEqual(location.cf[cf.key], cf.default)
617
655
 
618
656
  def test_create_single_object_with_values(self):
619
657
  """
@@ -624,51 +662,35 @@ class CustomFieldDataAPITest(APITestCase):
624
662
  "status": self.statuses[0].pk,
625
663
  "location_type": self.lt.pk,
626
664
  "custom_fields": {
627
- "text_cf": "bar",
628
- "number_cf": 456,
629
- "boolean_cf": True,
630
- "date_cf": "2020-01-02",
631
- "url_cf": "http://example.com/2",
632
- "choice_cf": "Bar",
633
- "multi_choice_cf": ["Baz"],
665
+ self.cf_text.key: "bar",
666
+ self.cf_integer.key: 456,
667
+ self.cf_boolean.key: True,
668
+ self.cf_date.key: "2020-01-02",
669
+ self.cf_url.key: "http://example.com/2",
670
+ self.cf_select.key: "Bar",
671
+ self.cf_multi_select.key: ["Baz"],
672
+ self.cf_markdown.key: "[hello](http://example.com)",
673
+ self.cf_json.key: {"foo": "bar"},
634
674
  },
635
675
  }
636
676
  if "example_plugin" in settings.PLUGINS:
637
677
  data["custom_fields"]["example_plugin_auto_custom_field"] = "Custom value"
638
- url = reverse("dcim-api:location-list")
639
- self.add_permissions("dcim.add_location")
640
-
641
- response = self.client.post(url, data, format="json", **self.header)
678
+ response = self.client.post(self.list_url, data, format="json", **self.header)
642
679
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
643
680
 
644
681
  # Validate response data
645
682
  response_cf = response.data["custom_fields"]
646
683
  data_cf = data["custom_fields"]
647
- self.assertEqual(response_cf["text_cf"], data_cf["text_cf"])
648
- self.assertEqual(response_cf["number_cf"], data_cf["number_cf"])
649
- self.assertEqual(response_cf["boolean_cf"], data_cf["boolean_cf"])
650
- self.assertEqual(response_cf["date_cf"], data_cf["date_cf"])
651
- self.assertEqual(response_cf["url_cf"], data_cf["url_cf"])
652
- self.assertEqual(response_cf["choice_cf"], data_cf["choice_cf"])
653
- self.assertEqual(response_cf["multi_choice_cf"], data_cf["multi_choice_cf"])
654
- if "example_plugin" in settings.PLUGINS:
655
- self.assertEqual(
656
- response_cf["example_plugin_auto_custom_field"], data_cf["example_plugin_auto_custom_field"]
657
- )
684
+ for cf in self.all_cfs:
685
+ self.assertIn(cf.key, response_cf)
686
+ self.assertIn(cf.key, data_cf)
687
+ self.assertEqual(response_cf[cf.key], data_cf[cf.key])
658
688
 
659
689
  # Validate database data
660
690
  location = Location.objects.get(pk=response.data["id"])
661
- self.assertEqual(location.cf["text_cf"], data_cf["text_cf"])
662
- self.assertEqual(location.cf["number_cf"], data_cf["number_cf"])
663
- self.assertEqual(location.cf["boolean_cf"], data_cf["boolean_cf"])
664
- self.assertEqual(str(location.cf["date_cf"]), data_cf["date_cf"])
665
- self.assertEqual(location.cf["url_cf"], data_cf["url_cf"])
666
- self.assertEqual(location.cf["choice_cf"], data_cf["choice_cf"])
667
- self.assertEqual(location.cf["multi_choice_cf"], data_cf["multi_choice_cf"])
668
- if "example_plugin" in settings.PLUGINS:
669
- self.assertEqual(
670
- location.cf["example_plugin_auto_custom_field"], data_cf["example_plugin_auto_custom_field"]
671
- )
691
+ for cf in self.all_cfs:
692
+ self.assertIn(cf.key, location.cf)
693
+ self.assertEqual(location.cf[cf.key], data_cf[cf.key])
672
694
 
673
695
  def test_create_multiple_objects_with_defaults(self):
674
696
  """
@@ -692,50 +714,37 @@ class CustomFieldDataAPITest(APITestCase):
692
714
  "status": self.statuses[0].pk,
693
715
  },
694
716
  )
695
- url = reverse("dcim-api:location-list")
696
- self.add_permissions("dcim.add_location")
697
-
698
- response = self.client.post(url, data, format="json", **self.header)
717
+ response = self.client.post(self.list_url, data, format="json", **self.header)
699
718
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
700
719
  self.assertEqual(len(response.data), len(data))
701
720
 
702
721
  for i, _obj in enumerate(data):
703
722
  # Validate response data
704
723
  response_cf = response.data[i]["custom_fields"]
705
- self.assertEqual(response_cf["text_cf"], self.cf_text.default)
706
- self.assertEqual(response_cf["number_cf"], self.cf_integer.default)
707
- self.assertEqual(response_cf["boolean_cf"], self.cf_boolean.default)
708
- self.assertEqual(response_cf["date_cf"], self.cf_date.default)
709
- self.assertEqual(response_cf["url_cf"], self.cf_url.default)
710
- self.assertEqual(response_cf["choice_cf"], self.cf_select.default)
711
- self.assertEqual(response_cf["multi_choice_cf"], self.cf_multi_select.default)
712
- if "example_plugin" in settings.PLUGINS:
713
- self.assertEqual(response_cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
724
+ for cf in self.all_cfs:
725
+ self.assertIn(cf.key, response_cf)
726
+ self.assertEqual(response_cf[cf.key], cf.default)
714
727
 
715
728
  # Validate database data
716
729
  location = Location.objects.get(pk=response.data[i]["id"])
717
- self.assertEqual(location.cf["text_cf"], self.cf_text.default)
718
- self.assertEqual(location.cf["number_cf"], self.cf_integer.default)
719
- self.assertEqual(location.cf["boolean_cf"], self.cf_boolean.default)
720
- self.assertEqual(str(location.cf["date_cf"]), self.cf_date.default)
721
- self.assertEqual(location.cf["url_cf"], self.cf_url.default)
722
- self.assertEqual(location.cf["choice_cf"], self.cf_select.default)
723
- self.assertEqual(location.cf["multi_choice_cf"], self.cf_multi_select.default)
724
- if "example_plugin" in settings.PLUGINS:
725
- self.assertEqual(location.cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
730
+ for cf in self.all_cfs:
731
+ self.assertIn(cf.key, location.cf)
732
+ self.assertEqual(location.cf[cf.key], cf.default)
726
733
 
727
734
  def test_create_multiple_objects_with_values(self):
728
735
  """
729
736
  Create a three new locations, each with custom fields defined.
730
737
  """
731
738
  custom_field_data = {
732
- "text_cf": "bar",
733
- "number_cf": 456,
734
- "boolean_cf": True,
735
- "date_cf": "2020-01-02",
736
- "url_cf": "http://example.com/2",
737
- "choice_cf": "Bar",
738
- "multi_choice_cf": ["Foo", "Bar"],
739
+ self.cf_text.key: "bar",
740
+ self.cf_integer.key: 456,
741
+ self.cf_boolean.key: True,
742
+ self.cf_date.key: "2020-01-02",
743
+ self.cf_url.key: "http://example.com/2",
744
+ self.cf_select.key: "Bar",
745
+ self.cf_multi_select.key: ["Foo", "Bar"],
746
+ self.cf_markdown.key: "### Heading",
747
+ self.cf_json.key: {"dict1": {"dict2": {}}},
739
748
  }
740
749
  if "example_plugin" in settings.PLUGINS:
741
750
  custom_field_data["example_plugin_auto_custom_field"] = "Custom value"
@@ -759,43 +768,23 @@ class CustomFieldDataAPITest(APITestCase):
759
768
  "custom_fields": custom_field_data,
760
769
  },
761
770
  )
762
- url = reverse("dcim-api:location-list")
763
- self.add_permissions("dcim.add_location")
764
-
765
- response = self.client.post(url, data, format="json", **self.header)
771
+ response = self.client.post(self.list_url, data, format="json", **self.header)
766
772
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
767
773
  self.assertEqual(len(response.data), len(data))
768
774
 
769
775
  for i, _obj in enumerate(data):
770
776
  # Validate response data
771
777
  response_cf = response.data[i]["custom_fields"]
772
- self.assertEqual(response_cf["text_cf"], custom_field_data["text_cf"])
773
- self.assertEqual(response_cf["number_cf"], custom_field_data["number_cf"])
774
- self.assertEqual(response_cf["boolean_cf"], custom_field_data["boolean_cf"])
775
- self.assertEqual(response_cf["date_cf"], custom_field_data["date_cf"])
776
- self.assertEqual(response_cf["url_cf"], custom_field_data["url_cf"])
777
- self.assertEqual(response_cf["choice_cf"], custom_field_data["choice_cf"])
778
- self.assertEqual(response_cf["multi_choice_cf"], custom_field_data["multi_choice_cf"])
779
- if "example_plugin" in settings.PLUGINS:
780
- self.assertEqual(
781
- response_cf["example_plugin_auto_custom_field"],
782
- custom_field_data["example_plugin_auto_custom_field"],
783
- )
778
+ for cf in self.all_cfs:
779
+ self.assertIn(cf.key, response_cf)
780
+ self.assertIn(cf.key, custom_field_data)
781
+ self.assertEqual(response_cf[cf.key], custom_field_data[cf.key])
784
782
 
785
783
  # Validate database data
786
784
  location = Location.objects.get(pk=response.data[i]["id"])
787
- self.assertEqual(location.cf["text_cf"], custom_field_data["text_cf"])
788
- self.assertEqual(location.cf["number_cf"], custom_field_data["number_cf"])
789
- self.assertEqual(location.cf["boolean_cf"], custom_field_data["boolean_cf"])
790
- self.assertEqual(str(location.cf["date_cf"]), custom_field_data["date_cf"])
791
- self.assertEqual(location.cf["url_cf"], custom_field_data["url_cf"])
792
- self.assertEqual(location.cf["choice_cf"], custom_field_data["choice_cf"])
793
- self.assertEqual(location.cf["multi_choice_cf"], custom_field_data["multi_choice_cf"])
794
- if "example_plugin" in settings.PLUGINS:
795
- self.assertEqual(
796
- location.cf["example_plugin_auto_custom_field"],
797
- custom_field_data["example_plugin_auto_custom_field"],
798
- )
785
+ for cf in self.all_cfs:
786
+ self.assertIn(cf.key, location.cf)
787
+ self.assertEqual(location.cf[cf.key], custom_field_data[cf.key])
799
788
 
800
789
  def test_update_single_object_with_values(self):
801
790
  """
@@ -806,114 +795,150 @@ class CustomFieldDataAPITest(APITestCase):
806
795
  original_cfvs = {**location.cf}
807
796
  data = {
808
797
  "custom_fields": {
809
- "text_cf": "ABCD",
810
- "number_cf": 1234,
798
+ self.cf_text.key: "ABCD",
799
+ self.cf_integer.key: 1234,
811
800
  },
812
801
  }
813
- url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
814
- self.add_permissions("dcim.change_location")
815
-
816
- response = self.client.patch(url, data, format="json", **self.header)
802
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
817
803
  self.assertHttpStatus(response, status.HTTP_200_OK)
818
804
 
819
805
  # Validate response data
820
806
  response_cf = response.data["custom_fields"]
821
- self.assertEqual(response_cf["text_cf"], data["custom_fields"]["text_cf"])
822
- self.assertEqual(response_cf["number_cf"], data["custom_fields"]["number_cf"])
823
- self.assertEqual(response_cf["boolean_cf"], original_cfvs["boolean_cf"])
824
- self.assertEqual(response_cf["date_cf"], original_cfvs["date_cf"])
825
- self.assertEqual(response_cf["url_cf"], original_cfvs["url_cf"])
826
- self.assertEqual(response_cf["choice_cf"], original_cfvs["choice_cf"])
827
- self.assertEqual(response_cf["multi_choice_cf"], original_cfvs["multi_choice_cf"])
828
- if "example_plugin" in settings.PLUGINS:
829
- self.assertEqual(
830
- response_cf["example_plugin_auto_custom_field"], original_cfvs["example_plugin_auto_custom_field"]
831
- )
807
+ for cf in self.all_cfs:
808
+ if cf.key in data["custom_fields"]:
809
+ self.assertEqual(response_cf[cf.key], data["custom_fields"][cf.key])
810
+ else:
811
+ self.assertEqual(response_cf[cf.key], original_cfvs[cf.key])
832
812
 
833
813
  # Validate database data
834
814
  location.refresh_from_db()
835
- self.assertEqual(location.cf["text_cf"], data["custom_fields"]["text_cf"])
836
- self.assertEqual(
837
- location.cf["number_cf"],
838
- data["custom_fields"]["number_cf"],
839
- )
840
- self.assertEqual(location.cf["boolean_cf"], original_cfvs["boolean_cf"])
841
- self.assertEqual(location.cf["date_cf"], original_cfvs["date_cf"])
842
- self.assertEqual(location.cf["url_cf"], original_cfvs["url_cf"])
843
- self.assertEqual(location.cf["choice_cf"], original_cfvs["choice_cf"])
844
- self.assertEqual(location.cf["multi_choice_cf"], original_cfvs["multi_choice_cf"])
845
- if "example_plugin" in settings.PLUGINS:
846
- self.assertEqual(
847
- location.cf["example_plugin_auto_custom_field"], original_cfvs["example_plugin_auto_custom_field"]
848
- )
849
-
850
- def test_minimum_maximum_values_validation(self):
851
- url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
852
- self.add_permissions("dcim.change_location")
815
+ for cf in self.all_cfs:
816
+ if cf.key in data["custom_fields"]:
817
+ self.assertEqual(location.cf[cf.key], data["custom_fields"][cf.key])
818
+ else:
819
+ self.assertEqual(location.cf[cf.key], original_cfvs[cf.key])
853
820
 
821
+ def test_integer_minimum_maximum_values_validation(self):
854
822
  self.cf_integer.validation_minimum = 10
855
823
  self.cf_integer.validation_maximum = 20
856
824
  self.cf_integer.save()
857
825
 
858
- data = {"custom_fields": {"number_cf": 9}}
859
- response = self.client.patch(url, data, format="json", **self.header)
826
+ data = {"custom_fields": {self.cf_integer.key: 9}}
827
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
860
828
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
861
829
 
862
- data = {"custom_fields": {"number_cf": 21}}
863
- response = self.client.patch(url, data, format="json", **self.header)
830
+ data = {"custom_fields": {self.cf_integer.key: 21}}
831
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
864
832
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
865
833
 
866
- data = {"custom_fields": {"number_cf": 15}}
867
- response = self.client.patch(url, data, format="json", **self.header)
834
+ data = {"custom_fields": {self.cf_integer.key: 15}}
835
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
868
836
  self.assertHttpStatus(response, status.HTTP_200_OK)
869
837
 
870
- def test_bigint_values_of_custom_field_maximum_attribute(self):
871
- url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
872
- self.add_permissions("dcim.change_location")
873
-
838
+ def test_integer_bigint_values_of_custom_field_maximum_attribute(self):
874
839
  self.cf_integer.validation_maximum = 5000000000
875
840
  self.cf_integer.save()
876
841
 
877
- data = {"custom_fields": {"number_cf": 4294967294}}
878
- response = self.client.patch(url, data, format="json", **self.header)
842
+ data = {"custom_fields": {self.cf_integer.key: 4294967294}}
843
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
879
844
  self.assertHttpStatus(response, status.HTTP_200_OK)
880
845
 
881
- data = {"custom_fields": {"number_cf": 5000000001}}
882
- response = self.client.patch(url, data, format="json", **self.header)
846
+ data = {"custom_fields": {self.cf_integer.key: 5000000001}}
847
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
883
848
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
884
849
 
885
- def test_bigint_values_of_custom_field_minimum_attribute(self):
886
- url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
887
- self.add_permissions("dcim.change_location")
888
-
850
+ def test_integer_bigint_values_of_custom_field_minimum_attribute(self):
889
851
  self.cf_integer.validation_minimum = -5000000000
890
852
  self.cf_integer.save()
891
853
 
892
- data = {"custom_fields": {"number_cf": -4294967294}}
893
- response = self.client.patch(url, data, format="json", **self.header)
854
+ data = {"custom_fields": {self.cf_integer.key: -4294967294}}
855
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
894
856
  self.assertHttpStatus(response, status.HTTP_200_OK)
895
857
 
896
- data = {"custom_fields": {"number_cf": -5000000001}}
897
- response = self.client.patch(url, data, format="json", **self.header)
858
+ data = {"custom_fields": {self.cf_integer.key: -5000000001}}
859
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
898
860
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
899
861
 
900
- def test_regex_validation(self):
901
- url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
902
- self.add_permissions("dcim.change_location")
862
+ def test_text_minimum_maximum_length_validation(self):
863
+ # No minimum or maximum length by default
864
+ data = {
865
+ "custom_fields": {
866
+ self.cf_text.key: "",
867
+ self.cf_url.key: "",
868
+ self.cf_json.key: "",
869
+ self.cf_markdown.key: "",
870
+ }
871
+ }
872
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
873
+ self.assertHttpStatus(response, status.HTTP_200_OK)
874
+
875
+ data = {
876
+ "custom_fields": {
877
+ self.cf_text.key: "a" * 500,
878
+ self.cf_url.key: "b" * 500,
879
+ self.cf_json.key: "c" * 500,
880
+ self.cf_markdown.key: "d" * 500,
881
+ }
882
+ }
883
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
884
+ self.assertHttpStatus(response, status.HTTP_200_OK)
885
+
886
+ for cf in [self.cf_text, self.cf_url, self.cf_json, self.cf_markdown]:
887
+ if cf != self.cf_json:
888
+ cf.validation_minimum = len(cf.default)
889
+ invalid_value = cf.default[:-1]
890
+ else:
891
+ cf.validation_minimum = len(json.dumps(cf.default))
892
+ invalid_value = {}
893
+ cf.validated_save()
903
894
 
895
+ try:
896
+ invalid_data = {"custom_fields": {cf.key: invalid_value}}
897
+ response = self.client.patch(self.detail_url, invalid_data, format="json", **self.header)
898
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
899
+
900
+ valid_data = {"custom_fields": {cf.key: cf.default}}
901
+ response = self.client.patch(self.detail_url, valid_data, format="json", **self.header)
902
+ self.assertHttpStatus(response, status.HTTP_200_OK)
903
+ finally:
904
+ cf.validation_minimum = None
905
+ cf.validated_save()
906
+
907
+ for cf in [self.cf_text, self.cf_url, self.cf_json, self.cf_markdown]:
908
+ if cf != self.cf_json:
909
+ cf.validation_maximum = len(cf.default)
910
+ invalid_value = cf.default + "1"
911
+ else:
912
+ cf.validation_maximum = len(json.dumps(cf.default))
913
+ invalid_value = json.dumps(cf.default) + "1"
914
+ cf.validated_save()
915
+
916
+ try:
917
+ invalid_data = {"custom_fields": {cf.key: invalid_value}}
918
+ response = self.client.patch(self.detail_url, invalid_data, format="json", **self.header)
919
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
920
+
921
+ valid_data = {"custom_fields": {cf.key: cf.default}}
922
+ response = self.client.patch(self.detail_url, valid_data, format="json", **self.header)
923
+ self.assertHttpStatus(response, status.HTTP_200_OK)
924
+ finally:
925
+ cf.validation_maximum = None
926
+ cf.validated_save()
927
+
928
+ def test_regex_validation(self):
904
929
  self.cf_text.validation_regex = r"^[A-Z]{3}$" # Three uppercase letters
905
930
  self.cf_text.save()
906
931
 
907
- data = {"custom_fields": {"text_cf": "ABC123"}}
908
- response = self.client.patch(url, data, format="json", **self.header)
932
+ data = {"custom_fields": {self.cf_text.key: "ABC123"}}
933
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
909
934
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
910
935
 
911
- data = {"custom_fields": {"text_cf": "abc"}}
912
- response = self.client.patch(url, data, format="json", **self.header)
936
+ data = {"custom_fields": {self.cf_text.key: "abc"}}
937
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
913
938
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
914
939
 
915
- data = {"custom_fields": {"text_cf": "ABC"}}
916
- response = self.client.patch(url, data, format="json", **self.header)
940
+ data = {"custom_fields": {self.cf_text.key: "ABC"}}
941
+ response = self.client.patch(self.detail_url, data, format="json", **self.header)
917
942
  self.assertHttpStatus(response, status.HTTP_200_OK)
918
943
 
919
944
  def test_select_regex_validation(self):
@@ -935,6 +960,26 @@ class CustomFieldDataAPITest(APITestCase):
935
960
  response = self.client.post(url, data, format="json", **self.header)
936
961
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
937
962
 
963
+ def test_select_minimum_maximum_validation(self):
964
+ url = reverse("extras-api:customfieldchoice-list")
965
+ self.add_permissions("extras.add_customfieldchoice")
966
+
967
+ self.cf_select.validation_minimum = len(self.cf_select.default)
968
+ self.cf_select.validation_maximum = len(self.cf_select.default)
969
+ self.cf_select.save()
970
+
971
+ data = {"custom_field": self.cf_select.id, "value": self.cf_select.default[:-1], "weight": 100}
972
+ response = self.client.post(url, data, format="json", **self.header)
973
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
974
+
975
+ data = {"custom_field": self.cf_select.id, "value": self.cf_select.default + "A", "weight": 100}
976
+ response = self.client.post(url, data, format="json", **self.header)
977
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
978
+
979
+ data = {"custom_field": self.cf_select.id, "value": "q" * len(self.cf_select.default), "weight": 100}
980
+ response = self.client.post(url, data, format="json", **self.header)
981
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
982
+
938
983
  def test_text_type_with_invalid_values(self):
939
984
  """
940
985
  Try and create a new location with an invalid value for a text type.
@@ -944,23 +989,20 @@ class CustomFieldDataAPITest(APITestCase):
944
989
  "status": self.statuses[0].pk,
945
990
  "location_type": self.lt.pk,
946
991
  "custom_fields": {
947
- "text_cf": ["I", "am", "a", "disallowed", "type"],
992
+ self.cf_text.key: ["I", "am", "a", "disallowed", "type"],
948
993
  },
949
994
  }
950
- url = reverse("dcim-api:location-list")
951
- self.add_permissions("dcim.add_location")
952
-
953
- response = self.client.post(url, data, format="json", **self.header)
995
+ response = self.client.post(self.list_url, data, format="json", **self.header)
954
996
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
955
997
  self.assertIn("Value must be a string", str(response.content))
956
998
 
957
- data["custom_fields"].update({"text_cf": 2})
958
- response = self.client.post(url, data, format="json", **self.header)
999
+ data["custom_fields"].update({self.cf_text.key: 2})
1000
+ response = self.client.post(self.list_url, data, format="json", **self.header)
959
1001
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
960
1002
  self.assertIn("Value must be a string", str(response.content))
961
1003
 
962
- data["custom_fields"].update({"text_cf": True})
963
- response = self.client.post(url, data, format="json", **self.header)
1004
+ data["custom_fields"].update({self.cf_text.key: True})
1005
+ response = self.client.post(self.list_url, data, format="json", **self.header)
964
1006
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
965
1007
  self.assertIn("Value must be a string", str(response.content))
966
1008
 
@@ -974,9 +1016,7 @@ class CustomFieldDataAPITest(APITestCase):
974
1016
  "location_type": self.lt.pk,
975
1017
  "status": self.statuses[0].pk,
976
1018
  }
977
- url = reverse("dcim-api:location-list")
978
- self.add_permissions("dcim.add_location")
979
- response = self.client.post(url, data, format="json", **self.header)
1019
+ response = self.client.post(self.list_url, data, format="json", **self.header)
980
1020
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
981
1021
  self.assertIn("Required field cannot be empty", str(response.content))
982
1022
 
@@ -987,7 +1027,7 @@ class CustomFieldDataAPITest(APITestCase):
987
1027
  f"Location N,{self.lt.composite_key},{self.statuses[0].name}",
988
1028
  ]
989
1029
  )
990
- response = self.client.post(url, csvdata, content_type="text/csv", **self.header)
1030
+ response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
991
1031
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
992
1032
  self.assertIn("Required field cannot be empty", str(response.content))
993
1033
 
@@ -997,12 +1037,10 @@ class CustomFieldDataAPITest(APITestCase):
997
1037
  "location_type": self.lt.pk,
998
1038
  "status": self.statuses[0].pk,
999
1039
  "custom_fields": {
1000
- "choice_cf": "Frobozz",
1040
+ self.cf_select.key: "Frobozz",
1001
1041
  },
1002
1042
  }
1003
- url = reverse("dcim-api:location-list")
1004
- self.add_permissions("dcim.add_location")
1005
- response = self.client.post(url, data, format="json", **self.header)
1043
+ response = self.client.post(self.list_url, data, format="json", **self.header)
1006
1044
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1007
1045
  self.assertIn("Invalid choice", str(response.content))
1008
1046
 
@@ -1013,7 +1051,7 @@ class CustomFieldDataAPITest(APITestCase):
1013
1051
  f"Location N,{self.lt.composite_key},{self.statuses[0].name},Frobozz",
1014
1052
  ]
1015
1053
  )
1016
- response = self.client.post(url, csvdata, content_type="text/csv", **self.header)
1054
+ response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
1017
1055
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1018
1056
  self.assertIn("Invalid choice", str(response.content))
1019
1057
 
@@ -1262,7 +1300,6 @@ class CustomFieldModelTest(TestCase):
1262
1300
  """Test that omitting required custom fields raises a ValidationError."""
1263
1301
  label = "Custom Field"
1264
1302
  custom_field = CustomField.objects.create(
1265
- # 2.0 TODO: #824 remove name field
1266
1303
  label=label,
1267
1304
  key="custom_field",
1268
1305
  type=CustomFieldTypeChoices.TYPE_TEXT,
@@ -1279,7 +1316,6 @@ class CustomFieldModelTest(TestCase):
1279
1316
  """Test that removing required custom fields and then updating an object raises a ValidationError."""
1280
1317
  label = "Custom Field"
1281
1318
  custom_field = CustomField.objects.create(
1282
- # 2.0 TODO: #824 remove name field
1283
1319
  label=label,
1284
1320
  key="custom_field",
1285
1321
  type=CustomFieldTypeChoices.TYPE_TEXT,
@@ -2158,7 +2194,7 @@ class CustomFieldTableTest(TestCase):
2158
2194
  for col_name, col_expected_value in custom_column_expected.items():
2159
2195
  internal_col_name = "cf_" + col_name
2160
2196
  custom_column = location_table.base_columns.get(internal_col_name)
2161
- self.assertIsNotNone(custom_column)
2197
+ self.assertIsNotNone(custom_column, internal_col_name)
2162
2198
  self.assertIsInstance(custom_column, CustomFieldColumn)
2163
2199
 
2164
2200
  rendered_value = bound_row.get_cell(internal_col_name)