arthexis 0.1.19__tar.gz → 0.1.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

Files changed (251) hide show
  1. {arthexis-0.1.19 → arthexis-0.1.20}/PKG-INFO +3 -3
  2. {arthexis-0.1.19 → arthexis-0.1.20}/README.md +2 -2
  3. {arthexis-0.1.19 → arthexis-0.1.20}/arthexis.egg-info/PKG-INFO +3 -3
  4. {arthexis-0.1.19 → arthexis-0.1.20}/arthexis.egg-info/SOURCES.txt +2 -0
  5. {arthexis-0.1.19 → arthexis-0.1.20}/core/admin.py +142 -1
  6. {arthexis-0.1.19 → arthexis-0.1.20}/core/backends.py +8 -2
  7. arthexis-0.1.20/core/environment.py +278 -0
  8. {arthexis-0.1.19 → arthexis-0.1.20}/core/models.py +124 -25
  9. {arthexis-0.1.19 → arthexis-0.1.20}/core/notifications.py +1 -1
  10. {arthexis-0.1.19 → arthexis-0.1.20}/core/reference_utils.py +10 -11
  11. {arthexis-0.1.19 → arthexis-0.1.20}/core/sigil_builder.py +2 -2
  12. {arthexis-0.1.19 → arthexis-0.1.20}/core/tasks.py +24 -1
  13. {arthexis-0.1.19 → arthexis-0.1.20}/core/tests.py +1 -0
  14. {arthexis-0.1.19 → arthexis-0.1.20}/core/views.py +70 -36
  15. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/admin.py +133 -1
  16. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/models.py +294 -48
  17. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/rfid_sync.py +1 -1
  18. arthexis-0.1.20/nodes/tasks.py +144 -0
  19. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/tests.py +532 -15
  20. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/urls.py +4 -0
  21. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/views.py +500 -95
  22. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/admin.py +101 -3
  23. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/consumers.py +106 -9
  24. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/models.py +83 -1
  25. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/tasks.py +4 -0
  26. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/test_export_import.py +1 -0
  27. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/test_rfid.py +3 -1
  28. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/tests.py +100 -9
  29. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/transactions_io.py +9 -1
  30. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/urls.py +3 -3
  31. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/views.py +101 -28
  32. {arthexis-0.1.19 → arthexis-0.1.20}/pages/context_processors.py +15 -9
  33. {arthexis-0.1.19 → arthexis-0.1.20}/pages/defaults.py +1 -1
  34. {arthexis-0.1.19 → arthexis-0.1.20}/pages/module_defaults.py +5 -5
  35. {arthexis-0.1.19 → arthexis-0.1.20}/pages/tests.py +110 -38
  36. {arthexis-0.1.19 → arthexis-0.1.20}/pages/urls.py +1 -0
  37. {arthexis-0.1.19 → arthexis-0.1.20}/pages/views.py +108 -8
  38. {arthexis-0.1.19 → arthexis-0.1.20}/pyproject.toml +1 -1
  39. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_client_report.py +5 -0
  40. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_allowed_hosts_hostname.py +3 -0
  41. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_auto_upgrade_scheduler.py +4 -0
  42. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_celery_no_debug.py +1 -0
  43. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_client_report_form.py +14 -0
  44. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_client_report_generation.py +5 -0
  45. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_client_report_schedule.py +5 -0
  46. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_email_inbox.py +1 -1
  47. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_email_transaction.py +1 -1
  48. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_install_script.py +3 -0
  49. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_invitation_login_view.py +4 -0
  50. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_node_info_view.py +3 -0
  51. arthexis-0.1.20/tests/test_readme_editor.py +58 -0
  52. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_build_flow.py +1 -0
  53. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_logs.py +8 -1
  54. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_progress.py +37 -0
  55. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_push.py +62 -0
  56. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_tasks.py +118 -4
  57. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_render_nginx_sites.py +5 -0
  58. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_admin_print_labels.py +84 -1
  59. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_always_on.py +3 -0
  60. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_backend.py +46 -1
  61. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_seed_data.py +3 -1
  62. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_settings_helpers.py +21 -0
  63. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_sigil_resolution.py +5 -0
  64. arthexis-0.1.20/tests/test_suite_gateway.py +127 -0
  65. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_urls_autodiscover.py +1 -1
  66. arthexis-0.1.19/core/environment.py +0 -61
  67. arthexis-0.1.19/nodes/tasks.py +0 -46
  68. {arthexis-0.1.19 → arthexis-0.1.20}/LICENSE +0 -0
  69. {arthexis-0.1.19 → arthexis-0.1.20}/arthexis.egg-info/dependency_links.txt +0 -0
  70. {arthexis-0.1.19 → arthexis-0.1.20}/arthexis.egg-info/requires.txt +0 -0
  71. {arthexis-0.1.19 → arthexis-0.1.20}/arthexis.egg-info/top_level.txt +0 -0
  72. {arthexis-0.1.19 → arthexis-0.1.20}/config/__init__.py +0 -0
  73. {arthexis-0.1.19 → arthexis-0.1.20}/config/active_app.py +0 -0
  74. {arthexis-0.1.19 → arthexis-0.1.20}/config/asgi.py +0 -0
  75. {arthexis-0.1.19 → arthexis-0.1.20}/config/auth_app.py +0 -0
  76. {arthexis-0.1.19 → arthexis-0.1.20}/config/celery.py +0 -0
  77. {arthexis-0.1.19 → arthexis-0.1.20}/config/context_processors.py +0 -0
  78. {arthexis-0.1.19 → arthexis-0.1.20}/config/horologia_app.py +0 -0
  79. {arthexis-0.1.19 → arthexis-0.1.20}/config/loadenv.py +0 -0
  80. {arthexis-0.1.19 → arthexis-0.1.20}/config/logging.py +0 -0
  81. {arthexis-0.1.19 → arthexis-0.1.20}/config/middleware.py +0 -0
  82. {arthexis-0.1.19 → arthexis-0.1.20}/config/offline.py +0 -0
  83. {arthexis-0.1.19 → arthexis-0.1.20}/config/settings.py +0 -0
  84. {arthexis-0.1.19 → arthexis-0.1.20}/config/settings_helpers.py +0 -0
  85. {arthexis-0.1.19 → arthexis-0.1.20}/config/urls.py +0 -0
  86. {arthexis-0.1.19 → arthexis-0.1.20}/config/wsgi.py +0 -0
  87. {arthexis-0.1.19 → arthexis-0.1.20}/core/__init__.py +0 -0
  88. {arthexis-0.1.19 → arthexis-0.1.20}/core/admin_history.py +0 -0
  89. {arthexis-0.1.19 → arthexis-0.1.20}/core/admindocs.py +0 -0
  90. {arthexis-0.1.19 → arthexis-0.1.20}/core/apps.py +0 -0
  91. {arthexis-0.1.19 → arthexis-0.1.20}/core/auto_upgrade.py +0 -0
  92. {arthexis-0.1.19 → arthexis-0.1.20}/core/changelog.py +0 -0
  93. {arthexis-0.1.19 → arthexis-0.1.20}/core/entity.py +0 -0
  94. {arthexis-0.1.19 → arthexis-0.1.20}/core/fields.py +0 -0
  95. {arthexis-0.1.19 → arthexis-0.1.20}/core/form_fields.py +0 -0
  96. {arthexis-0.1.19 → arthexis-0.1.20}/core/github_helper.py +0 -0
  97. {arthexis-0.1.19 → arthexis-0.1.20}/core/github_issues.py +0 -0
  98. {arthexis-0.1.19 → arthexis-0.1.20}/core/github_repos.py +0 -0
  99. {arthexis-0.1.19 → arthexis-0.1.20}/core/lcd_screen.py +0 -0
  100. {arthexis-0.1.19 → arthexis-0.1.20}/core/liveupdate.py +0 -0
  101. {arthexis-0.1.19 → arthexis-0.1.20}/core/log_paths.py +0 -0
  102. {arthexis-0.1.19 → arthexis-0.1.20}/core/mailer.py +0 -0
  103. {arthexis-0.1.19 → arthexis-0.1.20}/core/middleware.py +0 -0
  104. {arthexis-0.1.19 → arthexis-0.1.20}/core/public_wifi.py +0 -0
  105. {arthexis-0.1.19 → arthexis-0.1.20}/core/release.py +0 -0
  106. {arthexis-0.1.19 → arthexis-0.1.20}/core/rfid_import_export.py +0 -0
  107. {arthexis-0.1.19 → arthexis-0.1.20}/core/sigil_context.py +0 -0
  108. {arthexis-0.1.19 → arthexis-0.1.20}/core/sigil_resolver.py +0 -0
  109. {arthexis-0.1.19 → arthexis-0.1.20}/core/system.py +0 -0
  110. {arthexis-0.1.19 → arthexis-0.1.20}/core/temp_passwords.py +0 -0
  111. {arthexis-0.1.19 → arthexis-0.1.20}/core/test_system_info.py +0 -0
  112. {arthexis-0.1.19 → arthexis-0.1.20}/core/tests_liveupdate.py +0 -0
  113. {arthexis-0.1.19 → arthexis-0.1.20}/core/urls.py +0 -0
  114. {arthexis-0.1.19 → arthexis-0.1.20}/core/user_data.py +0 -0
  115. {arthexis-0.1.19 → arthexis-0.1.20}/core/widgets.py +0 -0
  116. {arthexis-0.1.19 → arthexis-0.1.20}/core/workgroup_urls.py +0 -0
  117. {arthexis-0.1.19 → arthexis-0.1.20}/core/workgroup_views.py +0 -0
  118. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/__init__.py +0 -0
  119. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/apps.py +0 -0
  120. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/backends.py +0 -0
  121. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/dns.py +0 -0
  122. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/feature_checks.py +0 -0
  123. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/lcd.py +0 -0
  124. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/reports.py +0 -0
  125. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/signals.py +0 -0
  126. {arthexis-0.1.19 → arthexis-0.1.20}/nodes/utils.py +0 -0
  127. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/__init__.py +0 -0
  128. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/apps.py +0 -0
  129. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/evcs.py +0 -0
  130. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/evcs_discovery.py +0 -0
  131. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/reference_utils.py +0 -0
  132. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/routing.py +0 -0
  133. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/simulator.py +0 -0
  134. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/status_display.py +0 -0
  135. {arthexis-0.1.19 → arthexis-0.1.20}/ocpp/store.py +0 -0
  136. {arthexis-0.1.19 → arthexis-0.1.20}/pages/__init__.py +0 -0
  137. {arthexis-0.1.19 → arthexis-0.1.20}/pages/admin.py +0 -0
  138. {arthexis-0.1.19 → arthexis-0.1.20}/pages/apps.py +0 -0
  139. {arthexis-0.1.19 → arthexis-0.1.20}/pages/checks.py +0 -0
  140. {arthexis-0.1.19 → arthexis-0.1.20}/pages/forms.py +0 -0
  141. {arthexis-0.1.19 → arthexis-0.1.20}/pages/middleware.py +0 -0
  142. {arthexis-0.1.19 → arthexis-0.1.20}/pages/models.py +0 -0
  143. {arthexis-0.1.19 → arthexis-0.1.20}/pages/site_config.py +0 -0
  144. {arthexis-0.1.19 → arthexis-0.1.20}/pages/tasks.py +0 -0
  145. {arthexis-0.1.19 → arthexis-0.1.20}/pages/utils.py +0 -0
  146. {arthexis-0.1.19 → arthexis-0.1.20}/setup.cfg +0 -0
  147. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_acronym_capitalization.py +0 -0
  148. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_doc_commands.py +0 -0
  149. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_doc_model_groups.py +0 -0
  150. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_history.py +0 -0
  151. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_index_actions.py +0 -0
  152. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_model_graph.py +0 -0
  153. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_object_history.py +0 -0
  154. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_profile_link.py +0 -0
  155. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_admin_system_stop.py +0 -0
  156. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_api_login_required.py +0 -0
  157. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_assistant_data_api.py +0 -0
  158. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_assistant_profile_admin.py +0 -0
  159. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_assistant_profile_api.py +0 -0
  160. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_awg_admin.py +0 -0
  161. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_benchmark_command.py +0 -0
  162. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_build_pypi_command.py +0 -0
  163. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_changelog_builder.py +0 -0
  164. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_check_admin_command.py +0 -0
  165. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_check_migrations_script.py +0 -0
  166. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_check_pypi_command.py +0 -0
  167. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_clean_release_logs_command.py +0 -0
  168. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_csrf_failure.py +0 -0
  169. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_csrf_origin_subnet.py +0 -0
  170. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_dist_cleanup.py +0 -0
  171. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_email_collector.py +0 -0
  172. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_email_inbox_admin.py +0 -0
  173. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_email_inbox_search_action.py +0 -0
  174. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_email_outbox_admin.py +0 -0
  175. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_email_profiles.py +0 -0
  176. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_env_refresh_clean.py +0 -0
  177. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_env_refresh_pip.py +0 -0
  178. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_env_refresh_unlink.py +0 -0
  179. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_experience_admin_group.py +0 -0
  180. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_fixture_presence.py +0 -0
  181. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_footer_no_references.py +0 -0
  182. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_footer_presence.py +0 -0
  183. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_footer_render.py +0 -0
  184. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_git_checks.py +0 -0
  185. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_github_issue_reporting.py +0 -0
  186. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_language_switch.py +0 -0
  187. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_lcd_check_command.py +0 -0
  188. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_lcd_smbus2.py +0 -0
  189. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_localhost_admin_backend.py +0 -0
  190. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_log_paths.py +0 -0
  191. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_login_view_no_site.py +0 -0
  192. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_manage_debug_flag.py +0 -0
  193. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_manuals.py +0 -0
  194. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_mcp_asgi.py +0 -0
  195. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_mcp_auto_start.py +0 -0
  196. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_mcp_process.py +0 -0
  197. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_mcp_sigil_server.py +0 -0
  198. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_mcp_sigil_server_command.py +0 -0
  199. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_message_command.py +0 -0
  200. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_migrations.py +0 -0
  201. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_model_verbose_name_capitalization.py +0 -0
  202. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_network_setup_interactive.py +0 -0
  203. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_notifications_fallback.py +0 -0
  204. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_notify_command.py +0 -0
  205. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_ocpp_session_lock.py +0 -0
  206. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_odoo_product.py +0 -0
  207. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_odoo_profile.py +0 -0
  208. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_odoo_profile_admin.py +0 -0
  209. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_odoo_quote_report.py +0 -0
  210. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_offline.py +0 -0
  211. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_package_admin_next_release.py +0 -0
  212. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_power_admin_group.py +0 -0
  213. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_profile_inline_deletion.py +0 -0
  214. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_pypi_check.py +0 -0
  215. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_pypi_token.py +0 -0
  216. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_readme_language.py +0 -0
  217. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_reference_qr_code.py +0 -0
  218. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_reference_transaction_uuid.py +0 -0
  219. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_register_site_apps_command.py +0 -0
  220. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_build.py +0 -0
  221. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_checklist.py +0 -0
  222. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_manager_admin.py +0 -0
  223. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_packages.py +0 -0
  224. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_release_progress_pre_release_integration.py +0 -0
  225. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_request_invite.py +0 -0
  226. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_admin_reference_clear.py +0 -0
  227. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_admin_scan_csrf.py +0 -0
  228. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_background_reader.py +0 -0
  229. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_client_report.py +0 -0
  230. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_rfid_watch_command.py +0 -0
  231. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_role_marker_filtering.py +0 -0
  232. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_send_invite_command.py +0 -0
  233. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_shell_scripts.py +0 -0
  234. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_show_leads_command.py +0 -0
  235. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_sigil_builder.py +0 -0
  236. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_sites_utils.py +0 -0
  237. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_social_profile.py +0 -0
  238. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_staff_login_net_message.py +0 -0
  239. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_staff_required_decorator.py +0 -0
  240. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_switch_role_script.py +0 -0
  241. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_system_changelog_report.py +0 -0
  242. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_temp_passwords.py +0 -0
  243. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_totp_admin.py +0 -0
  244. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_totp_backend.py +0 -0
  245. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_uninstall_script.py +0 -0
  246. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_update_fixtures_command.py +0 -0
  247. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_upgrade_report.py +0 -0
  248. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_user_data_admin.py +0 -0
  249. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_version_endpoint.py +0 -0
  250. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_version_file.py +0 -0
  251. {arthexis-0.1.19 → arthexis-0.1.20}/tests/test_vscode_manage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.19
3
+ Version: 0.1.20
4
4
  Summary: Power & Energy Infrastructure
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -171,7 +171,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
171
171
  <td valign="top"><strong>Multi-Device Edge, Network &amp; Data Acquisition</strong><br />Features: AP Router, Celery Queue, NGINX Server, RFID Scanner</td>
172
172
  </tr>
173
173
  <tr>
174
- <td valign="top"><strong>Constellation</strong></td>
174
+ <td valign="top"><strong>Watchtower</strong></td>
175
175
  <td valign="top"><strong>Multi-User Cloud &amp; Orchestration</strong><br />Features: Celery Queue, NGINX Server</td>
176
176
  </tr>
177
177
  </tbody>
@@ -184,7 +184,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
184
184
  - **[Windows](https://en.wikipedia.org/wiki/Microsoft_Windows)**: open [PowerShell](https://learn.microsoft.com/powershell/) or [Git Bash](https://gitforwindows.org/) and run the same command.
185
185
 
186
186
  ### 2. Start and stop
187
- Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Constellation roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
187
+ Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Watchtower roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
188
188
 
189
189
  - **[VS Code](https://code.visualstudio.com/)**
190
190
  - Open the folder and go to the **Run and Debug** panel (`Ctrl+Shift+D`).
@@ -55,7 +55,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
55
55
  <td valign="top"><strong>Multi-Device Edge, Network &amp; Data Acquisition</strong><br />Features: AP Router, Celery Queue, NGINX Server, RFID Scanner</td>
56
56
  </tr>
57
57
  <tr>
58
- <td valign="top"><strong>Constellation</strong></td>
58
+ <td valign="top"><strong>Watchtower</strong></td>
59
59
  <td valign="top"><strong>Multi-User Cloud &amp; Orchestration</strong><br />Features: Celery Queue, NGINX Server</td>
60
60
  </tr>
61
61
  </tbody>
@@ -68,7 +68,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
68
68
  - **[Windows](https://en.wikipedia.org/wiki/Microsoft_Windows)**: open [PowerShell](https://learn.microsoft.com/powershell/) or [Git Bash](https://gitforwindows.org/) and run the same command.
69
69
 
70
70
  ### 2. Start and stop
71
- Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Constellation roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
71
+ Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Watchtower roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
72
72
 
73
73
  - **[VS Code](https://code.visualstudio.com/)**
74
74
  - Open the folder and go to the **Run and Debug** panel (`Ctrl+Shift+D`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.19
3
+ Version: 0.1.20
4
4
  Summary: Power & Energy Infrastructure
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -171,7 +171,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
171
171
  <td valign="top"><strong>Multi-Device Edge, Network &amp; Data Acquisition</strong><br />Features: AP Router, Celery Queue, NGINX Server, RFID Scanner</td>
172
172
  </tr>
173
173
  <tr>
174
- <td valign="top"><strong>Constellation</strong></td>
174
+ <td valign="top"><strong>Watchtower</strong></td>
175
175
  <td valign="top"><strong>Multi-User Cloud &amp; Orchestration</strong><br />Features: Celery Queue, NGINX Server</td>
176
176
  </tr>
177
177
  </tbody>
@@ -184,7 +184,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
184
184
  - **[Windows](https://en.wikipedia.org/wiki/Microsoft_Windows)**: open [PowerShell](https://learn.microsoft.com/powershell/) or [Git Bash](https://gitforwindows.org/) and run the same command.
185
185
 
186
186
  ### 2. Start and stop
187
- Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Constellation roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
187
+ Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Watchtower roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
188
188
 
189
189
  - **[VS Code](https://code.visualstudio.com/)**
190
190
  - Open the folder and go to the **Run and Debug** panel (`Ctrl+Shift+D`).
@@ -194,6 +194,7 @@ tests/test_power_admin_group.py
194
194
  tests/test_profile_inline_deletion.py
195
195
  tests/test_pypi_check.py
196
196
  tests/test_pypi_token.py
197
+ tests/test_readme_editor.py
197
198
  tests/test_readme_language.py
198
199
  tests/test_reference_qr_code.py
199
200
  tests/test_reference_transaction_uuid.py
@@ -230,6 +231,7 @@ tests/test_sites_utils.py
230
231
  tests/test_social_profile.py
231
232
  tests/test_staff_login_net_message.py
232
233
  tests/test_staff_required_decorator.py
234
+ tests/test_suite_gateway.py
233
235
  tests/test_switch_role_script.py
234
236
  tests/test_system_changelog_report.py
235
237
  tests/test_temp_passwords.py
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from io import BytesIO
2
3
  import os
3
4
 
@@ -2909,7 +2910,7 @@ class CopyRFIDForm(forms.Form):
2909
2910
  normalized = (cleaned or "").strip().upper()
2910
2911
  if not normalized:
2911
2912
  raise forms.ValidationError(_("RFID value is required."))
2912
- if RFID.objects.filter(rfid=normalized).exists():
2913
+ if RFID.matching_queryset(normalized).exists():
2913
2914
  raise forms.ValidationError(
2914
2915
  _("An RFID with this value already exists.")
2915
2916
  )
@@ -2943,6 +2944,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2943
2944
  "print_card_labels",
2944
2945
  "print_release_form",
2945
2946
  "copy_rfids",
2947
+ "merge_rfids",
2946
2948
  "toggle_selected_user_data",
2947
2949
  "toggle_selected_released",
2948
2950
  "toggle_selected_allowed",
@@ -3177,6 +3179,145 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3177
3179
  context["media"] = self.media + form.media
3178
3180
  return TemplateResponse(request, "admin/core/rfid/copy.html", context)
3179
3181
 
3182
+ @admin.action(description=_("Merge RFID cards"))
3183
+ def merge_rfids(self, request, queryset):
3184
+ tags = list(queryset.prefetch_related("energy_accounts"))
3185
+ if len(tags) < 2:
3186
+ self.message_user(
3187
+ request,
3188
+ _("Select at least two RFIDs to merge."),
3189
+ level=messages.WARNING,
3190
+ )
3191
+ return None
3192
+
3193
+ normalized_map: dict[int, str] = {}
3194
+ groups: defaultdict[str, list[RFID]] = defaultdict(list)
3195
+ unmatched = 0
3196
+ for tag in tags:
3197
+ normalized = RFID.normalize_code(tag.rfid)
3198
+ normalized_map[tag.pk] = normalized
3199
+ if not normalized:
3200
+ unmatched += 1
3201
+ continue
3202
+ prefix = normalized[: RFID.MATCH_PREFIX_LENGTH]
3203
+ groups[prefix].append(tag)
3204
+
3205
+ merge_groups: list[list[RFID]] = []
3206
+ skipped = unmatched
3207
+ for prefix, group in groups.items():
3208
+ if len(group) < 2:
3209
+ skipped += len(group)
3210
+ continue
3211
+ group.sort(
3212
+ key=lambda item: (
3213
+ len(normalized_map.get(item.pk, "")),
3214
+ normalized_map.get(item.pk, ""),
3215
+ item.pk,
3216
+ )
3217
+ )
3218
+ merge_groups.append(group)
3219
+
3220
+ if not merge_groups:
3221
+ self.message_user(
3222
+ request,
3223
+ _("No matching RFIDs were found to merge."),
3224
+ level=messages.WARNING,
3225
+ )
3226
+ return None
3227
+
3228
+ merged_tags = 0
3229
+ merged_groups = 0
3230
+ conflicting_accounts = 0
3231
+ with transaction.atomic():
3232
+ for group in merge_groups:
3233
+ canonical = group[0]
3234
+ update_fields: set[str] = set()
3235
+ existing_account_ids = set(
3236
+ canonical.energy_accounts.values_list("pk", flat=True)
3237
+ )
3238
+ for tag in group[1:]:
3239
+ other_value = normalized_map.get(tag.pk, "")
3240
+ if canonical.adopt_rfid(other_value):
3241
+ update_fields.add("rfid")
3242
+ normalized_map[canonical.pk] = RFID.normalize_code(
3243
+ canonical.rfid
3244
+ )
3245
+ accounts = list(tag.energy_accounts.all())
3246
+ if accounts:
3247
+ transferable: list[EnergyAccount] = []
3248
+ for account in accounts:
3249
+ if existing_account_ids and account.pk not in existing_account_ids:
3250
+ conflicting_accounts += 1
3251
+ continue
3252
+ transferable.append(account)
3253
+ if transferable:
3254
+ canonical.energy_accounts.add(*transferable)
3255
+ existing_account_ids.update(
3256
+ account.pk for account in transferable
3257
+ )
3258
+ if tag.allowed and not canonical.allowed:
3259
+ canonical.allowed = True
3260
+ update_fields.add("allowed")
3261
+ if tag.released and not canonical.released:
3262
+ canonical.released = True
3263
+ update_fields.add("released")
3264
+ if tag.key_a_verified and not canonical.key_a_verified:
3265
+ canonical.key_a_verified = True
3266
+ update_fields.add("key_a_verified")
3267
+ if tag.key_b_verified and not canonical.key_b_verified:
3268
+ canonical.key_b_verified = True
3269
+ update_fields.add("key_b_verified")
3270
+ if tag.last_seen_on and (
3271
+ not canonical.last_seen_on
3272
+ or tag.last_seen_on > canonical.last_seen_on
3273
+ ):
3274
+ canonical.last_seen_on = tag.last_seen_on
3275
+ update_fields.add("last_seen_on")
3276
+ if not canonical.origin_node and tag.origin_node_id:
3277
+ canonical.origin_node = tag.origin_node
3278
+ update_fields.add("origin_node")
3279
+ merged_tags += 1
3280
+ tag.delete()
3281
+ if update_fields:
3282
+ canonical.save(update_fields=sorted(update_fields))
3283
+ merged_groups += 1
3284
+
3285
+ if merged_tags:
3286
+ self.message_user(
3287
+ request,
3288
+ ngettext(
3289
+ "Merged %(removed)d RFID into %(groups)d canonical record.",
3290
+ "Merged %(removed)d RFIDs into %(groups)d canonical records.",
3291
+ merged_tags,
3292
+ )
3293
+ % {"removed": merged_tags, "groups": merged_groups},
3294
+ level=messages.SUCCESS,
3295
+ )
3296
+
3297
+ if skipped:
3298
+ self.message_user(
3299
+ request,
3300
+ ngettext(
3301
+ "Skipped %(count)d RFID because it did not share the first %(length)d characters with another selection.",
3302
+ "Skipped %(count)d RFIDs because they did not share the first %(length)d characters with another selection.",
3303
+ skipped,
3304
+ )
3305
+ % {"count": skipped, "length": RFID.MATCH_PREFIX_LENGTH},
3306
+ level=messages.WARNING,
3307
+ )
3308
+
3309
+ if conflicting_accounts:
3310
+ self.message_user(
3311
+ request,
3312
+ ngettext(
3313
+ "Skipped %(count)d energy account because the RFID was already linked to a different account.",
3314
+ "Skipped %(count)d energy accounts because the RFID was already linked to a different account.",
3315
+ conflicting_accounts,
3316
+ )
3317
+ % {"count": conflicting_accounts},
3318
+ level=messages.WARNING,
3319
+ )
3320
+
3180
3321
  def _render_card_labels(
3181
3322
  self,
3182
3323
  request,
@@ -81,10 +81,16 @@ class RFIDBackend:
81
81
  if not rfid_value:
82
82
  return None
83
83
 
84
- tag = RFID.objects.filter(rfid=rfid_value).first()
85
- if not tag or not tag.allowed:
84
+ tag = RFID.matching_queryset(rfid_value).filter(allowed=True).first()
85
+ if not tag:
86
86
  return None
87
87
 
88
+ update_fields: list[str] = []
89
+ if tag.adopt_rfid(rfid_value):
90
+ update_fields.append("rfid")
91
+ if update_fields:
92
+ tag.save(update_fields=update_fields)
93
+
88
94
  command = (tag.external_command or "").strip()
89
95
  if command:
90
96
  env = os.environ.copy()
@@ -0,0 +1,278 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import shlex
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from django import forms
10
+ from django.conf import settings
11
+ from django.contrib import admin
12
+ from django.core.exceptions import PermissionDenied
13
+ from django.template.response import TemplateResponse
14
+ from django.urls import path, reverse
15
+ from django.utils.translation import gettext_lazy as _
16
+
17
+
18
+ def _get_django_settings():
19
+ return sorted(
20
+ [(name, getattr(settings, name)) for name in dir(settings) if name.isupper()]
21
+ )
22
+
23
+
24
+ class NetworkSetupForm(forms.Form):
25
+ prompt_for_password = forms.BooleanField(
26
+ label=_("Prompt for new WiFi password"),
27
+ required=False,
28
+ help_text=_("Add --password to request a password even when one is already configured."),
29
+ )
30
+ access_point_name = forms.CharField(
31
+ label=_("Access point name"),
32
+ required=False,
33
+ max_length=32,
34
+ help_text=_("Use --ap to set the wlan0 access point name."),
35
+ )
36
+ skip_firewall_validation = forms.BooleanField(
37
+ label=_("Skip firewall validation"),
38
+ required=False,
39
+ help_text=_("Add --no-firewall to bypass firewall port checks."),
40
+ )
41
+ skip_access_point_configuration = forms.BooleanField(
42
+ label=_("Skip access point configuration"),
43
+ required=False,
44
+ help_text=_("Add --no-ap to leave the access point configuration unchanged."),
45
+ )
46
+ allow_unsafe_changes = forms.BooleanField(
47
+ label=_("Allow modifying the active internet connection"),
48
+ required=False,
49
+ help_text=_("Include --unsafe to allow changes that may interrupt connectivity."),
50
+ )
51
+ interactive = forms.BooleanField(
52
+ label=_("Prompt before each step"),
53
+ required=False,
54
+ help_text=_("Run the script with --interactive to confirm each action."),
55
+ )
56
+ install_watchdog = forms.BooleanField(
57
+ label=_("Install WiFi watchdog service"),
58
+ required=False,
59
+ initial=True,
60
+ help_text=_("Keep selected to retain the watchdog or clear to add --no-watchdog."),
61
+ )
62
+ vnc_validation = forms.ChoiceField(
63
+ label=_("VNC validation"),
64
+ choices=(
65
+ ("default", _("Use script default (skip validation)")),
66
+ ("require", _("Require that a VNC service is enabled (--vnc)")),
67
+ ),
68
+ initial="default",
69
+ required=True,
70
+ )
71
+ ethernet_subnet = forms.CharField(
72
+ label=_("Ethernet subnet"),
73
+ required=False,
74
+ help_text=_("Provide N or N/P (prefix 16 or 24) to supply --subnet."),
75
+ )
76
+ update_ap_password_only = forms.BooleanField(
77
+ label=_("Update access point password only"),
78
+ required=False,
79
+ help_text=_("Use --ap-set-password without running other setup steps."),
80
+ )
81
+
82
+ def clean_ethernet_subnet(self) -> str:
83
+ value = self.cleaned_data.get("ethernet_subnet", "")
84
+ if not value:
85
+ return ""
86
+ raw = value.strip()
87
+ match = re.fullmatch(r"(?P<subnet>\d{1,3})(?:/(?P<prefix>\d{1,2}))?", raw)
88
+ if not match:
89
+ raise forms.ValidationError(
90
+ _("Enter a subnet in the form N or N/P with prefix 16 or 24."),
91
+ )
92
+ subnet = int(match.group("subnet"))
93
+ if subnet < 0 or subnet > 254:
94
+ raise forms.ValidationError(
95
+ _("Subnet value must be between 0 and 254."),
96
+ )
97
+ prefix_value = match.group("prefix")
98
+ if prefix_value:
99
+ prefix = int(prefix_value)
100
+ if prefix not in {16, 24}:
101
+ raise forms.ValidationError(
102
+ _("Subnet prefix must be 16 or 24."),
103
+ )
104
+ return f"{subnet}/{prefix}"
105
+ return str(subnet)
106
+
107
+ def clean(self) -> dict:
108
+ cleaned_data = super().clean()
109
+ if cleaned_data.get("update_ap_password_only"):
110
+ other_flags = [
111
+ cleaned_data.get("prompt_for_password"),
112
+ bool(cleaned_data.get("access_point_name")),
113
+ cleaned_data.get("skip_firewall_validation"),
114
+ cleaned_data.get("skip_access_point_configuration"),
115
+ cleaned_data.get("allow_unsafe_changes"),
116
+ cleaned_data.get("interactive"),
117
+ bool(cleaned_data.get("ethernet_subnet")),
118
+ cleaned_data.get("vnc_validation") == "require",
119
+ not cleaned_data.get("install_watchdog", True),
120
+ ]
121
+ if any(other_flags):
122
+ raise forms.ValidationError(
123
+ _(
124
+ "Update access point password only cannot be combined with other network-setup options."
125
+ )
126
+ )
127
+ return cleaned_data
128
+
129
+ def build_command(self, script_path: Path) -> list[str]:
130
+ command = [str(script_path)]
131
+ data = self.cleaned_data
132
+ if data.get("update_ap_password_only"):
133
+ command.append("--ap-set-password")
134
+ return command
135
+ if data.get("prompt_for_password"):
136
+ command.append("--password")
137
+ access_point_name = data.get("access_point_name")
138
+ if access_point_name:
139
+ command.extend(["--ap", access_point_name])
140
+ if data.get("skip_firewall_validation"):
141
+ command.append("--no-firewall")
142
+ if data.get("skip_access_point_configuration"):
143
+ command.append("--no-ap")
144
+ if data.get("allow_unsafe_changes"):
145
+ command.append("--unsafe")
146
+ if data.get("interactive"):
147
+ command.append("--interactive")
148
+ if not data.get("install_watchdog"):
149
+ command.append("--no-watchdog")
150
+ if data.get("vnc_validation") == "require":
151
+ command.append("--vnc")
152
+ ethernet_subnet = data.get("ethernet_subnet")
153
+ if ethernet_subnet:
154
+ command.extend(["--subnet", ethernet_subnet])
155
+ return command
156
+
157
+
158
+ def _environment_view(request):
159
+ env_vars = sorted(os.environ.items())
160
+ context = admin.site.each_context(request)
161
+ environment_tasks: list[dict[str, str]] = []
162
+ if request.user.is_superuser:
163
+ environment_tasks.append(
164
+ {
165
+ "name": _("Run network-setup"),
166
+ "description": _(
167
+ "Configure network services, stage managed NGINX sites, and review script output."
168
+ ),
169
+ "url": reverse("admin:environment-network-setup"),
170
+ }
171
+ )
172
+ context.update(
173
+ {
174
+ "title": _("Environment"),
175
+ "env_vars": env_vars,
176
+ "environment_tasks": environment_tasks,
177
+ }
178
+ )
179
+ return TemplateResponse(request, "admin/environment.html", context)
180
+
181
+
182
+ def _environment_network_setup_view(request):
183
+ if not request.user.is_superuser:
184
+ raise PermissionDenied
185
+
186
+ script_path = Path(settings.BASE_DIR) / "network-setup.sh"
187
+ command_result: dict[str, object] | None = None
188
+
189
+ if request.method == "POST":
190
+ form = NetworkSetupForm(request.POST)
191
+ if form.is_valid():
192
+ command = form.build_command(script_path)
193
+ if not script_path.exists():
194
+ form.add_error(None, _("The network-setup.sh script could not be found."))
195
+ else:
196
+ try:
197
+ completed = subprocess.run(
198
+ command,
199
+ capture_output=True,
200
+ text=True,
201
+ cwd=settings.BASE_DIR,
202
+ check=False,
203
+ )
204
+ except FileNotFoundError:
205
+ form.add_error(None, _("The network-setup.sh script could not be executed."))
206
+ except OSError as exc:
207
+ form.add_error(
208
+ None,
209
+ _("Unable to execute network-setup.sh: %(error)s")
210
+ % {"error": str(exc)},
211
+ )
212
+ else:
213
+ if hasattr(shlex, "join"):
214
+ command_display = shlex.join(command)
215
+ else:
216
+ command_display = " ".join(shlex.quote(part) for part in command)
217
+ command_result = {
218
+ "command": command_display,
219
+ "stdout": completed.stdout,
220
+ "stderr": completed.stderr,
221
+ "returncode": completed.returncode,
222
+ "succeeded": completed.returncode == 0,
223
+ }
224
+ else:
225
+ form = NetworkSetupForm()
226
+
227
+ context = admin.site.each_context(request)
228
+ context.update(
229
+ {
230
+ "title": _("Run network-setup"),
231
+ "form": form,
232
+ "command_result": command_result,
233
+ "task_description": _(
234
+ "Configure script flags, execute network-setup, and review the captured output."
235
+ ),
236
+ "back_url": reverse("admin:environment"),
237
+ }
238
+ )
239
+ return TemplateResponse(request, "admin/environment_network_setup.html", context)
240
+
241
+
242
+ def _config_view(request):
243
+ context = admin.site.each_context(request)
244
+ context.update(
245
+ {
246
+ "title": _("Django Settings"),
247
+ "django_settings": _get_django_settings(),
248
+ }
249
+ )
250
+ return TemplateResponse(request, "admin/config.html", context)
251
+
252
+
253
+ def patch_admin_environment_view() -> None:
254
+ """Register the Environment and Config admin views on the main admin site."""
255
+ original_get_urls = admin.site.get_urls
256
+
257
+ def get_urls():
258
+ urls = original_get_urls()
259
+ custom = [
260
+ path(
261
+ "environment/network-setup/",
262
+ admin.site.admin_view(_environment_network_setup_view),
263
+ name="environment-network-setup",
264
+ ),
265
+ path(
266
+ "environment/",
267
+ admin.site.admin_view(_environment_view),
268
+ name="environment",
269
+ ),
270
+ path(
271
+ "config/",
272
+ admin.site.admin_view(_config_view),
273
+ name="config",
274
+ ),
275
+ ]
276
+ return custom + urls
277
+
278
+ admin.site.get_urls = get_urls