arthexis 0.1.11__tar.gz → 0.1.12__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 (211) hide show
  1. {arthexis-0.1.11 → arthexis-0.1.12}/PKG-INFO +2 -2
  2. {arthexis-0.1.11 → arthexis-0.1.12}/README.md +1 -1
  3. {arthexis-0.1.11 → arthexis-0.1.12}/arthexis.egg-info/PKG-INFO +2 -2
  4. {arthexis-0.1.11 → arthexis-0.1.12}/arthexis.egg-info/SOURCES.txt +5 -1
  5. {arthexis-0.1.11 → arthexis-0.1.12}/config/settings.py +7 -2
  6. {arthexis-0.1.11 → arthexis-0.1.12}/core/admin.py +246 -68
  7. {arthexis-0.1.11 → arthexis-0.1.12}/core/apps.py +21 -0
  8. {arthexis-0.1.11 → arthexis-0.1.12}/core/models.py +41 -8
  9. {arthexis-0.1.11 → arthexis-0.1.12}/core/reference_utils.py +1 -1
  10. {arthexis-0.1.11 → arthexis-0.1.12}/core/release.py +4 -0
  11. {arthexis-0.1.11 → arthexis-0.1.12}/core/system.py +6 -3
  12. {arthexis-0.1.11 → arthexis-0.1.12}/core/tasks.py +92 -40
  13. {arthexis-0.1.11 → arthexis-0.1.12}/core/tests.py +64 -0
  14. {arthexis-0.1.11 → arthexis-0.1.12}/core/views.py +131 -17
  15. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/admin.py +316 -6
  16. arthexis-0.1.12/nodes/feature_checks.py +133 -0
  17. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/models.py +83 -26
  18. arthexis-0.1.12/nodes/reports.py +411 -0
  19. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/tests.py +365 -36
  20. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/utils.py +32 -0
  21. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/admin.py +278 -15
  22. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/consumers.py +506 -8
  23. arthexis-0.1.12/ocpp/evcs_discovery.py +158 -0
  24. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/models.py +234 -4
  25. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/simulator.py +321 -22
  26. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/store.py +110 -2
  27. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/tests.py +789 -6
  28. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/transactions_io.py +17 -3
  29. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/views.py +225 -19
  30. {arthexis-0.1.11 → arthexis-0.1.12}/pages/admin.py +135 -3
  31. {arthexis-0.1.11 → arthexis-0.1.12}/pages/context_processors.py +15 -1
  32. {arthexis-0.1.11 → arthexis-0.1.12}/pages/defaults.py +1 -2
  33. {arthexis-0.1.11 → arthexis-0.1.12}/pages/forms.py +38 -0
  34. {arthexis-0.1.11 → arthexis-0.1.12}/pages/models.py +136 -1
  35. {arthexis-0.1.11 → arthexis-0.1.12}/pages/tests.py +262 -4
  36. {arthexis-0.1.11 → arthexis-0.1.12}/pages/urls.py +1 -0
  37. {arthexis-0.1.11 → arthexis-0.1.12}/pages/views.py +52 -3
  38. {arthexis-0.1.11 → arthexis-0.1.12}/pyproject.toml +1 -1
  39. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_assistant_profile_admin.py +4 -1
  40. arthexis-0.1.12/tests/test_email_outbox_admin.py +124 -0
  41. arthexis-0.1.12/tests/test_manage_debug_flag.py +103 -0
  42. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_mcp_sigil_server.py +40 -1
  43. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_odoo_product.py +93 -3
  44. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_pypi_token.py +16 -10
  45. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_release_tasks.py +67 -12
  46. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_sigil_resolution.py +7 -2
  47. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_temp_passwords.py +25 -0
  48. arthexis-0.1.12/tests/test_totp_admin.py +67 -0
  49. arthexis-0.1.11/tests/test_email_outbox_admin.py +0 -43
  50. arthexis-0.1.11/tests/test_scan_evcs_consoles_command.py +0 -103
  51. {arthexis-0.1.11 → arthexis-0.1.12}/LICENSE +0 -0
  52. {arthexis-0.1.11 → arthexis-0.1.12}/arthexis.egg-info/dependency_links.txt +0 -0
  53. {arthexis-0.1.11 → arthexis-0.1.12}/arthexis.egg-info/requires.txt +0 -0
  54. {arthexis-0.1.11 → arthexis-0.1.12}/arthexis.egg-info/top_level.txt +0 -0
  55. {arthexis-0.1.11 → arthexis-0.1.12}/config/__init__.py +0 -0
  56. {arthexis-0.1.11 → arthexis-0.1.12}/config/active_app.py +0 -0
  57. {arthexis-0.1.11 → arthexis-0.1.12}/config/asgi.py +0 -0
  58. {arthexis-0.1.11 → arthexis-0.1.12}/config/auth_app.py +0 -0
  59. {arthexis-0.1.11 → arthexis-0.1.12}/config/celery.py +0 -0
  60. {arthexis-0.1.11 → arthexis-0.1.12}/config/context_processors.py +0 -0
  61. {arthexis-0.1.11 → arthexis-0.1.12}/config/horologia_app.py +0 -0
  62. {arthexis-0.1.11 → arthexis-0.1.12}/config/loadenv.py +0 -0
  63. {arthexis-0.1.11 → arthexis-0.1.12}/config/logging.py +0 -0
  64. {arthexis-0.1.11 → arthexis-0.1.12}/config/middleware.py +0 -0
  65. {arthexis-0.1.11 → arthexis-0.1.12}/config/offline.py +0 -0
  66. {arthexis-0.1.11 → arthexis-0.1.12}/config/urls.py +0 -0
  67. {arthexis-0.1.11 → arthexis-0.1.12}/config/wsgi.py +0 -0
  68. {arthexis-0.1.11/pages → arthexis-0.1.12/core}/__init__.py +0 -0
  69. {arthexis-0.1.11 → arthexis-0.1.12}/core/admin_history.py +0 -0
  70. {arthexis-0.1.11 → arthexis-0.1.12}/core/admindocs.py +0 -0
  71. {arthexis-0.1.11 → arthexis-0.1.12}/core/auto_upgrade.py +0 -0
  72. {arthexis-0.1.11 → arthexis-0.1.12}/core/backends.py +0 -0
  73. {arthexis-0.1.11 → arthexis-0.1.12}/core/entity.py +0 -0
  74. {arthexis-0.1.11 → arthexis-0.1.12}/core/environment.py +0 -0
  75. {arthexis-0.1.11 → arthexis-0.1.12}/core/fields.py +0 -0
  76. {arthexis-0.1.11 → arthexis-0.1.12}/core/github_helper.py +0 -0
  77. {arthexis-0.1.11 → arthexis-0.1.12}/core/github_issues.py +0 -0
  78. {arthexis-0.1.11 → arthexis-0.1.12}/core/lcd_screen.py +0 -0
  79. {arthexis-0.1.11 → arthexis-0.1.12}/core/liveupdate.py +0 -0
  80. {arthexis-0.1.11 → arthexis-0.1.12}/core/log_paths.py +0 -0
  81. {arthexis-0.1.11 → arthexis-0.1.12}/core/mailer.py +0 -0
  82. {arthexis-0.1.11 → arthexis-0.1.12}/core/middleware.py +0 -0
  83. {arthexis-0.1.11 → arthexis-0.1.12}/core/notifications.py +0 -0
  84. {arthexis-0.1.11 → arthexis-0.1.12}/core/public_wifi.py +0 -0
  85. {arthexis-0.1.11 → arthexis-0.1.12}/core/sigil_builder.py +0 -0
  86. {arthexis-0.1.11 → arthexis-0.1.12}/core/sigil_context.py +0 -0
  87. {arthexis-0.1.11 → arthexis-0.1.12}/core/sigil_resolver.py +0 -0
  88. {arthexis-0.1.11 → arthexis-0.1.12}/core/temp_passwords.py +0 -0
  89. {arthexis-0.1.11 → arthexis-0.1.12}/core/test_system_info.py +0 -0
  90. {arthexis-0.1.11 → arthexis-0.1.12}/core/tests_liveupdate.py +0 -0
  91. {arthexis-0.1.11 → arthexis-0.1.12}/core/urls.py +0 -0
  92. {arthexis-0.1.11 → arthexis-0.1.12}/core/user_data.py +0 -0
  93. {arthexis-0.1.11 → arthexis-0.1.12}/core/widgets.py +0 -0
  94. {arthexis-0.1.11 → arthexis-0.1.12}/core/workgroup_urls.py +0 -0
  95. {arthexis-0.1.11 → arthexis-0.1.12}/core/workgroup_views.py +0 -0
  96. {arthexis-0.1.11/ocpp → arthexis-0.1.12/nodes}/__init__.py +0 -0
  97. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/actions.py +0 -0
  98. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/apps.py +0 -0
  99. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/backends.py +0 -0
  100. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/dns.py +0 -0
  101. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/lcd.py +0 -0
  102. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/tasks.py +0 -0
  103. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/urls.py +0 -0
  104. {arthexis-0.1.11 → arthexis-0.1.12}/nodes/views.py +0 -0
  105. {arthexis-0.1.11/nodes → arthexis-0.1.12/ocpp}/__init__.py +0 -0
  106. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/apps.py +0 -0
  107. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/evcs.py +0 -0
  108. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/reference_utils.py +0 -0
  109. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/routing.py +0 -0
  110. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/tasks.py +0 -0
  111. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/test_export_import.py +0 -0
  112. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/test_rfid.py +0 -0
  113. {arthexis-0.1.11 → arthexis-0.1.12}/ocpp/urls.py +0 -0
  114. {arthexis-0.1.11/core → arthexis-0.1.12/pages}/__init__.py +0 -0
  115. {arthexis-0.1.11 → arthexis-0.1.12}/pages/apps.py +0 -0
  116. {arthexis-0.1.11 → arthexis-0.1.12}/pages/checks.py +0 -0
  117. {arthexis-0.1.11 → arthexis-0.1.12}/pages/middleware.py +0 -0
  118. {arthexis-0.1.11 → arthexis-0.1.12}/pages/utils.py +0 -0
  119. {arthexis-0.1.11 → arthexis-0.1.12}/setup.cfg +0 -0
  120. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_acronym_capitalization.py +0 -0
  121. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_client_report.py +0 -0
  122. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_doc_commands.py +0 -0
  123. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_doc_model_groups.py +0 -0
  124. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_history.py +0 -0
  125. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_index_actions.py +0 -0
  126. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_model_graph.py +0 -0
  127. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_object_history.py +0 -0
  128. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_profile_link.py +0 -0
  129. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_admin_system_stop.py +0 -0
  130. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_allowed_hosts_hostname.py +0 -0
  131. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_assistant_data_api.py +0 -0
  132. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_assistant_profile_api.py +0 -0
  133. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_auto_upgrade_scheduler.py +0 -0
  134. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_awg_admin.py +0 -0
  135. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_benchmark_command.py +0 -0
  136. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_birthday_greetings.py +0 -0
  137. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_celery_no_debug.py +0 -0
  138. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_check_migrations_script.py +0 -0
  139. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_client_report_generation.py +0 -0
  140. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_client_report_schedule.py +0 -0
  141. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_csrf_failure.py +0 -0
  142. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_csrf_origin_subnet.py +0 -0
  143. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_dist_cleanup.py +0 -0
  144. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_email_collector.py +0 -0
  145. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_email_inbox.py +0 -0
  146. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_email_inbox_admin.py +0 -0
  147. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_email_inbox_search_action.py +0 -0
  148. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_email_profiles.py +0 -0
  149. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_env_refresh_clean.py +0 -0
  150. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_env_refresh_pip.py +0 -0
  151. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_env_refresh_unlink.py +0 -0
  152. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_experience_admin_group.py +0 -0
  153. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_fixture_presence.py +0 -0
  154. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_footer_no_references.py +0 -0
  155. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_footer_presence.py +0 -0
  156. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_footer_render.py +0 -0
  157. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_git_checks.py +0 -0
  158. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_github_issue_reporting.py +0 -0
  159. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_install_script.py +0 -0
  160. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_language_switch.py +0 -0
  161. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_lcd_check_command.py +0 -0
  162. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_lcd_smbus2.py +0 -0
  163. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_localhost_admin_backend.py +0 -0
  164. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_login_view_no_site.py +0 -0
  165. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_manage_debug.py +0 -0
  166. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_manuals.py +0 -0
  167. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_mcp_process.py +0 -0
  168. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_message_command.py +0 -0
  169. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_migrations.py +0 -0
  170. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_model_verbose_name_capitalization.py +0 -0
  171. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_network_setup_interactive.py +0 -0
  172. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_node_info_view.py +0 -0
  173. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_notifications_fallback.py +0 -0
  174. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_notify_command.py +0 -0
  175. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_ocpp_session_lock.py +0 -0
  176. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_odoo_profile.py +0 -0
  177. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_odoo_profile_admin.py +0 -0
  178. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_offline.py +0 -0
  179. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_package_admin_next_release.py +0 -0
  180. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_power_admin_group.py +0 -0
  181. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_profile_inline_deletion.py +0 -0
  182. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_projects_rfid.py +0 -0
  183. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_readme_language.py +0 -0
  184. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_reference_qr_code.py +0 -0
  185. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_reference_transaction_uuid.py +0 -0
  186. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_register_site_apps_command.py +0 -0
  187. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_release_checklist.py +0 -0
  188. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_release_logs.py +0 -0
  189. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_release_manager_admin.py +0 -0
  190. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_release_progress.py +0 -0
  191. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_request_invite.py +0 -0
  192. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_rfid_admin_reference_clear.py +0 -0
  193. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_rfid_admin_scan_csrf.py +0 -0
  194. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_rfid_background_reader.py +0 -0
  195. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_rfid_client_report.py +0 -0
  196. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_role_marker_filtering.py +0 -0
  197. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_seed_data.py +0 -0
  198. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_send_invite_command.py +0 -0
  199. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_shell_scripts.py +0 -0
  200. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_show_leads_command.py +0 -0
  201. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_sigil_builder.py +0 -0
  202. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_sites_utils.py +0 -0
  203. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_staff_login_net_message.py +0 -0
  204. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_switch_role_script.py +0 -0
  205. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_uninstall_script.py +0 -0
  206. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_update_fixtures_command.py +0 -0
  207. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_urls_autodiscover.py +0 -0
  208. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_user_data_admin.py +0 -0
  209. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_version_endpoint.py +0 -0
  210. {arthexis-0.1.11 → arthexis-0.1.12}/tests/test_version_file.py +0 -0
  211. {arthexis-0.1.11 → arthexis-0.1.12}/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.11
3
+ Version: 0.1.12
4
4
  Summary: Django-based MESH system
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -115,7 +115,7 @@ Dynamic: license-file
115
115
 
116
116
  # Arthexis Constellation
117
117
 
118
- [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)
118
+ [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](docs/development/ocpp-user-manual.md)
119
119
 
120
120
  ## Purpose
121
121
 
@@ -1,6 +1,6 @@
1
1
  # Arthexis Constellation
2
2
 
3
- [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)
3
+ [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](docs/development/ocpp-user-manual.md)
4
4
 
5
5
  ## Purpose
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.11
3
+ Version: 0.1.12
4
4
  Summary: Django-based MESH system
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -115,7 +115,7 @@ Dynamic: license-file
115
115
 
116
116
  # Arthexis Constellation
117
117
 
118
- [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)
118
+ [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](docs/development/ocpp-user-manual.md)
119
119
 
120
120
  ## Purpose
121
121
 
@@ -63,8 +63,10 @@ nodes/admin.py
63
63
  nodes/apps.py
64
64
  nodes/backends.py
65
65
  nodes/dns.py
66
+ nodes/feature_checks.py
66
67
  nodes/lcd.py
67
68
  nodes/models.py
69
+ nodes/reports.py
68
70
  nodes/tasks.py
69
71
  nodes/tests.py
70
72
  nodes/urls.py
@@ -75,6 +77,7 @@ ocpp/admin.py
75
77
  ocpp/apps.py
76
78
  ocpp/consumers.py
77
79
  ocpp/evcs.py
80
+ ocpp/evcs_discovery.py
78
81
  ocpp/models.py
79
82
  ocpp/reference_utils.py
80
83
  ocpp/routing.py
@@ -148,6 +151,7 @@ tests/test_lcd_smbus2.py
148
151
  tests/test_localhost_admin_backend.py
149
152
  tests/test_login_view_no_site.py
150
153
  tests/test_manage_debug.py
154
+ tests/test_manage_debug_flag.py
151
155
  tests/test_manuals.py
152
156
  tests/test_mcp_process.py
153
157
  tests/test_mcp_sigil_server.py
@@ -183,7 +187,6 @@ tests/test_rfid_admin_scan_csrf.py
183
187
  tests/test_rfid_background_reader.py
184
188
  tests/test_rfid_client_report.py
185
189
  tests/test_role_marker_filtering.py
186
- tests/test_scan_evcs_consoles_command.py
187
190
  tests/test_seed_data.py
188
191
  tests/test_send_invite_command.py
189
192
  tests/test_shell_scripts.py
@@ -194,6 +197,7 @@ tests/test_sites_utils.py
194
197
  tests/test_staff_login_net_message.py
195
198
  tests/test_switch_role_script.py
196
199
  tests/test_temp_passwords.py
200
+ tests/test_totp_admin.py
197
201
  tests/test_uninstall_script.py
198
202
  tests/test_update_fixtures_command.py
199
203
  tests/test_urls_autodiscover.py
@@ -685,10 +685,15 @@ LOGGING = {
685
685
  "backupCount": 7,
686
686
  "encoding": "utf-8",
687
687
  "formatter": "standard",
688
- }
688
+ },
689
+ "console": {
690
+ "class": "logging.StreamHandler",
691
+ "level": "ERROR",
692
+ "formatter": "standard",
693
+ },
689
694
  },
690
695
  "root": {
691
- "handlers": ["file"],
696
+ "handlers": ["file", "console"],
692
697
  "level": "DEBUG",
693
698
  },
694
699
  }
@@ -76,6 +76,7 @@ from .user_data import (
76
76
  )
77
77
  from .widgets import OdooProductWidget
78
78
  from .mcp import process as mcp_process
79
+ from .mcp.server import resolve_base_urls
79
80
 
80
81
 
81
82
  admin.site.unregister(Group)
@@ -371,6 +372,29 @@ class ReleaseManagerAdminForm(forms.ModelForm):
371
372
  "github_token": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
372
373
  }
373
374
 
375
+ def __init__(self, *args, **kwargs):
376
+ super().__init__(*args, **kwargs)
377
+ self.fields["pypi_token"].help_text = format_html(
378
+ "{} <a href=\"{}\" target=\"_blank\" rel=\"noopener noreferrer\">{}</a>{}",
379
+ "Generate an API token from your PyPI account settings.",
380
+ "https://pypi.org/manage/account/token/",
381
+ "pypi.org/manage/account/token/",
382
+ (
383
+ " by clicking “Add API token”, optionally scoping it to the package, "
384
+ "and paste the full `pypi-***` value here."
385
+ ),
386
+ )
387
+ self.fields["github_token"].help_text = format_html(
388
+ "{} <a href=\"{}\" target=\"_blank\" rel=\"noopener noreferrer\">{}</a>{}",
389
+ "Create a personal access token at GitHub → Settings → Developer settings →",
390
+ "https://github.com/settings/tokens",
391
+ "github.com/settings/tokens",
392
+ (
393
+ " with the repository access needed for releases (repo scope for classic tokens "
394
+ "or an equivalent fine-grained token) and paste it here."
395
+ ),
396
+ )
397
+
374
398
 
375
399
  @admin.register(ReleaseManager)
376
400
  class ReleaseManagerAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
@@ -690,28 +714,30 @@ class OdooProfileAdminForm(forms.ModelForm):
690
714
  )
691
715
 
692
716
 
693
- class EmailInboxAdminForm(forms.ModelForm):
694
- """Admin form for :class:`core.models.EmailInbox` with hidden password."""
695
-
696
- password = forms.CharField(
697
- widget=forms.PasswordInput(render_value=True),
698
- required=False,
699
- help_text="Leave blank to keep the current password.",
700
- )
717
+ class MaskedPasswordFormMixin:
718
+ """Mixin that hides stored passwords while allowing updates."""
701
719
 
702
- class Meta:
703
- model = EmailInbox
704
- fields = "__all__"
720
+ password_sigil_fields: tuple[str, ...] = ()
705
721
 
706
722
  def __init__(self, *args, **kwargs):
707
723
  super().__init__(*args, **kwargs)
724
+ field = self.fields.get("password")
725
+ if field is None:
726
+ return
727
+ if not isinstance(field.widget, forms.PasswordInput):
728
+ field.widget = forms.PasswordInput()
729
+ field.widget.attrs.setdefault("autocomplete", "new-password")
730
+ field.help_text = field.help_text or "Leave blank to keep the current password."
708
731
  if self.instance.pk:
709
- self.fields["password"].initial = ""
732
+ field.initial = ""
710
733
  self.initial["password"] = ""
711
734
  else:
712
- self.fields["password"].required = True
735
+ field.required = True
713
736
 
714
737
  def clean_password(self):
738
+ field = self.fields.get("password")
739
+ if field is None:
740
+ return self.cleaned_data.get("password")
715
741
  pwd = self.cleaned_data.get("password")
716
742
  if not pwd and self.instance.pk:
717
743
  return keep_existing("password")
@@ -719,10 +745,23 @@ class EmailInboxAdminForm(forms.ModelForm):
719
745
 
720
746
  def _post_clean(self):
721
747
  super()._post_clean()
722
- _restore_sigil_values(
723
- self,
724
- ["username", "host", "password", "protocol"],
725
- )
748
+ if self.password_sigil_fields:
749
+ _restore_sigil_values(self, self.password_sigil_fields)
750
+
751
+
752
+ class EmailInboxAdminForm(MaskedPasswordFormMixin, forms.ModelForm):
753
+ """Admin form for :class:`core.models.EmailInbox` with hidden password."""
754
+
755
+ password = forms.CharField(
756
+ widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
757
+ required=False,
758
+ help_text="Leave blank to keep the current password.",
759
+ )
760
+ password_sigil_fields = ("username", "host", "password", "protocol")
761
+
762
+ class Meta:
763
+ model = EmailInbox
764
+ fields = "__all__"
726
765
 
727
766
 
728
767
  class ProfileInlineFormSet(BaseInlineFormSet):
@@ -880,16 +919,25 @@ class SocialProfileInlineForm(ProfileFormMixin, forms.ModelForm):
880
919
  fields = ("network", "handle", "domain", "did")
881
920
 
882
921
 
883
- class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
884
- profile_fields = EmailOutbox.profile_fields
922
+ class EmailOutboxAdminForm(MaskedPasswordFormMixin, forms.ModelForm):
923
+ """Admin form for :class:`nodes.models.EmailOutbox` with hidden password."""
924
+
885
925
  password = forms.CharField(
886
- widget=forms.PasswordInput(render_value=True),
926
+ widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
887
927
  required=False,
888
928
  help_text="Leave blank to keep the current password.",
889
929
  )
930
+ password_sigil_fields = ("password", "host", "username", "from_email")
890
931
 
891
932
  class Meta:
892
933
  model = EmailOutbox
934
+ fields = "__all__"
935
+
936
+
937
+ class EmailOutboxInlineForm(ProfileFormMixin, EmailOutboxAdminForm):
938
+ profile_fields = EmailOutbox.profile_fields
939
+
940
+ class Meta(EmailOutboxAdminForm.Meta):
893
941
  fields = (
894
942
  "password",
895
943
  "host",
@@ -901,27 +949,6 @@ class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
901
949
  "is_enabled",
902
950
  )
903
951
 
904
- def __init__(self, *args, **kwargs):
905
- super().__init__(*args, **kwargs)
906
- if self.instance.pk:
907
- self.fields["password"].initial = ""
908
- self.initial["password"] = ""
909
- else:
910
- self.fields["password"].required = True
911
-
912
- def clean_password(self):
913
- pwd = self.cleaned_data.get("password")
914
- if not pwd and self.instance.pk:
915
- return keep_existing("password")
916
- return pwd
917
-
918
- def _post_clean(self):
919
- super()._post_clean()
920
- _restore_sigil_values(
921
- self,
922
- ["password", "host", "username", "from_email"],
923
- )
924
-
925
952
 
926
953
  class ReleaseManagerInlineForm(ProfileFormMixin, forms.ModelForm):
927
954
  profile_fields = ReleaseManager.profile_fields
@@ -1324,10 +1351,8 @@ class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdm
1324
1351
  changelist_actions = ["my_profile"]
1325
1352
  fieldsets = (
1326
1353
  ("Owner", {"fields": ("user", "group")}),
1327
- (
1328
- "Configuration",
1329
- {"fields": ("host", "database", "username", "password")},
1330
- ),
1354
+ ("Configuration", {"fields": ("host", "database")}),
1355
+ ("Credentials", {"fields": ("username", "password")}),
1331
1356
  (
1332
1357
  "Odoo Employee",
1333
1358
  {"fields": ("verified_on", "odoo_uid", "name", "email")},
@@ -1417,18 +1442,10 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
1417
1442
 
1418
1443
  fieldsets = (
1419
1444
  ("Owner", {"fields": ("user", "group")}),
1445
+ ("Credentials", {"fields": ("username", "password")}),
1420
1446
  (
1421
- None,
1422
- {
1423
- "fields": (
1424
- "username",
1425
- "host",
1426
- "port",
1427
- "password",
1428
- "protocol",
1429
- "use_ssl",
1430
- )
1431
- },
1447
+ "Configuration",
1448
+ {"fields": ("host", "port", "protocol", "use_ssl")},
1432
1449
  ),
1433
1450
  )
1434
1451
 
@@ -1526,17 +1543,10 @@ class AssistantProfileAdmin(
1526
1543
  changelist_actions = ["my_profile"]
1527
1544
  fieldsets = (
1528
1545
  ("Owner", {"fields": ("user", "group")}),
1546
+ ("Credentials", {"fields": ("user_key_hash",)}),
1529
1547
  (
1530
- None,
1531
- {
1532
- "fields": (
1533
- "scopes",
1534
- "is_active",
1535
- "user_key_hash",
1536
- "created_at",
1537
- "last_used_at",
1538
- )
1539
- },
1548
+ "Configuration",
1549
+ {"fields": ("scopes", "is_active", "created_at", "last_used_at")},
1540
1550
  ),
1541
1551
  )
1542
1552
 
@@ -1627,14 +1637,19 @@ class AssistantProfileAdmin(
1627
1637
  config = dict(getattr(settings, "MCP_SIGIL_SERVER", {}))
1628
1638
  host = config.get("host") or "127.0.0.1"
1629
1639
  port = config.get("port", 8800)
1640
+ base_url, issuer_url = resolve_base_urls(config)
1630
1641
  if isinstance(response, dict):
1631
1642
  response.setdefault("mcp_server_host", host)
1632
1643
  response.setdefault("mcp_server_port", port)
1644
+ response.setdefault("mcp_server_base_url", base_url)
1645
+ response.setdefault("mcp_server_issuer_url", issuer_url)
1633
1646
  else:
1634
1647
  context_data = getattr(response, "context_data", None)
1635
1648
  if context_data is not None:
1636
1649
  context_data.setdefault("mcp_server_host", host)
1637
1650
  context_data.setdefault("mcp_server_port", port)
1651
+ context_data.setdefault("mcp_server_base_url", base_url)
1652
+ context_data.setdefault("mcp_server_issuer_url", issuer_url)
1638
1653
  return response
1639
1654
 
1640
1655
  def start_server(self, request):
@@ -1922,7 +1937,7 @@ class ProductFetchWizardForm(forms.Form):
1922
1937
  @admin.register(Product)
1923
1938
  class ProductAdmin(EntityModelAdmin):
1924
1939
  form = ProductAdminForm
1925
- actions = ["fetch_odoo_product"]
1940
+ actions = ["fetch_odoo_product", "register_from_odoo"]
1926
1941
 
1927
1942
  def _odoo_profile_admin(self):
1928
1943
  return self.admin_site._registry.get(OdooProfile)
@@ -1932,7 +1947,7 @@ class ProductAdmin(EntityModelAdmin):
1932
1947
  return profile.execute(
1933
1948
  "product.product",
1934
1949
  "search_read",
1935
- domain,
1950
+ [domain],
1936
1951
  {
1937
1952
  "fields": [
1938
1953
  "name",
@@ -2072,6 +2087,169 @@ class ProductAdmin(EntityModelAdmin):
2072
2087
  context["media"] = self.media + form.media
2073
2088
  return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
2074
2089
 
2090
+ def get_urls(self):
2091
+ urls = super().get_urls()
2092
+ custom = [
2093
+ path(
2094
+ "register-from-odoo/",
2095
+ self.admin_site.admin_view(self.register_from_odoo_view),
2096
+ name=f"{self.opts.app_label}_{self.opts.model_name}_register_from_odoo",
2097
+ )
2098
+ ]
2099
+ return custom + urls
2100
+
2101
+ @admin.action(description="Register from Odoo")
2102
+ def register_from_odoo(self, request, queryset=None): # pragma: no cover - simple redirect
2103
+ return HttpResponseRedirect(
2104
+ reverse(
2105
+ f"admin:{self.opts.app_label}_{self.opts.model_name}_register_from_odoo"
2106
+ )
2107
+ )
2108
+
2109
+ def _build_register_context(self, request):
2110
+ opts = self.model._meta
2111
+ context = self.admin_site.each_context(request)
2112
+ context.update(
2113
+ {
2114
+ "opts": opts,
2115
+ "title": _("Register from Odoo"),
2116
+ "has_credentials": False,
2117
+ "profile_url": None,
2118
+ "products": [],
2119
+ "selected_product_id": request.POST.get("product_id", ""),
2120
+ }
2121
+ )
2122
+
2123
+ profile_admin = self._odoo_profile_admin()
2124
+ if profile_admin is not None:
2125
+ context["profile_url"] = profile_admin.get_my_profile_url(request)
2126
+
2127
+ profile = getattr(request.user, "odoo_profile", None)
2128
+ if not profile or not profile.is_verified:
2129
+ context["credential_error"] = _(
2130
+ "Configure your Odoo employee credentials before registering products."
2131
+ )
2132
+ return context, None
2133
+
2134
+ try:
2135
+ products = profile.execute(
2136
+ "product.product",
2137
+ "search_read",
2138
+ [[]],
2139
+ {
2140
+ "fields": [
2141
+ "name",
2142
+ "description_sale",
2143
+ "list_price",
2144
+ "standard_price",
2145
+ ],
2146
+ "limit": 0,
2147
+ },
2148
+ )
2149
+ except Exception:
2150
+ context["error"] = _("Unable to fetch products from Odoo.")
2151
+ return context, []
2152
+
2153
+ context["has_credentials"] = True
2154
+ simplified = []
2155
+ for product in products:
2156
+ simplified.append(
2157
+ {
2158
+ "id": product.get("id"),
2159
+ "name": product.get("name", ""),
2160
+ "description_sale": product.get("description_sale", ""),
2161
+ "list_price": product.get("list_price"),
2162
+ "standard_price": product.get("standard_price"),
2163
+ }
2164
+ )
2165
+ context["products"] = simplified
2166
+ return context, simplified
2167
+
2168
+ def register_from_odoo_view(self, request):
2169
+ context, products = self._build_register_context(request)
2170
+ if products is None:
2171
+ return TemplateResponse(
2172
+ request, "admin/core/product/register_from_odoo.html", context
2173
+ )
2174
+
2175
+ if request.method == "POST" and context.get("has_credentials"):
2176
+ if not self.has_add_permission(request):
2177
+ context["form_error"] = _(
2178
+ "You do not have permission to add products."
2179
+ )
2180
+ else:
2181
+ product_id = request.POST.get("product_id")
2182
+ if not product_id:
2183
+ context["form_error"] = _("Select a product to register.")
2184
+ else:
2185
+ try:
2186
+ odoo_id = int(product_id)
2187
+ except (TypeError, ValueError):
2188
+ context["form_error"] = _("Invalid product selection.")
2189
+ else:
2190
+ match = next(
2191
+ (item for item in products if item.get("id") == odoo_id),
2192
+ None,
2193
+ )
2194
+ if not match:
2195
+ context["form_error"] = _(
2196
+ "The selected product was not found. Reload the page and try again."
2197
+ )
2198
+ else:
2199
+ existing = self.model.objects.filter(
2200
+ odoo_product__id=odoo_id
2201
+ ).first()
2202
+ if existing:
2203
+ self.message_user(
2204
+ request,
2205
+ _(
2206
+ "Product %(name)s already imported; opening existing record."
2207
+ )
2208
+ % {"name": existing.name},
2209
+ level=messages.WARNING,
2210
+ )
2211
+ return HttpResponseRedirect(
2212
+ reverse(
2213
+ "admin:%s_%s_change"
2214
+ % (
2215
+ existing._meta.app_label,
2216
+ existing._meta.model_name,
2217
+ ),
2218
+ args=[existing.pk],
2219
+ )
2220
+ )
2221
+ product = self.model.objects.create(
2222
+ name=match.get("name") or f"Odoo Product {odoo_id}",
2223
+ description=match.get("description_sale", "") or "",
2224
+ renewal_period=30,
2225
+ odoo_product={
2226
+ "id": odoo_id,
2227
+ "name": match.get("name", ""),
2228
+ },
2229
+ )
2230
+ self.log_addition(
2231
+ request, product, "Registered product from Odoo"
2232
+ )
2233
+ self.message_user(
2234
+ request,
2235
+ _("Imported %(name)s from Odoo.")
2236
+ % {"name": product.name},
2237
+ )
2238
+ return HttpResponseRedirect(
2239
+ reverse(
2240
+ "admin:%s_%s_change"
2241
+ % (
2242
+ product._meta.app_label,
2243
+ product._meta.model_name,
2244
+ ),
2245
+ args=[product.pk],
2246
+ )
2247
+ )
2248
+
2249
+ return TemplateResponse(
2250
+ request, "admin/core/product/register_from_odoo.html", context
2251
+ )
2252
+
2075
2253
 
2076
2254
  class RFIDResource(resources.ModelResource):
2077
2255
  reference = fields.Field(
@@ -21,6 +21,7 @@ class CoreConfig(AppConfig):
21
21
  from pathlib import Path
22
22
 
23
23
  from django.conf import settings
24
+ from django.core.exceptions import ObjectDoesNotExist
24
25
  from django.contrib.auth import get_user_model
25
26
  from django.db.models.signals import post_migrate
26
27
  from django.core.signals import got_request_exception
@@ -39,6 +40,26 @@ class CoreConfig(AppConfig):
39
40
  )
40
41
  from .admin_history import patch_admin_history
41
42
 
43
+ from django_otp.plugins.otp_totp.models import TOTPDevice as OTP_TOTPDevice
44
+
45
+ if not hasattr(
46
+ OTP_TOTPDevice._read_str_from_settings, "_core_totp_issuer_patch"
47
+ ):
48
+ original_read_str = OTP_TOTPDevice._read_str_from_settings
49
+
50
+ def _core_totp_read_str(self, key):
51
+ if key == "OTP_TOTP_ISSUER":
52
+ try:
53
+ settings_obj = self.custom_settings
54
+ except ObjectDoesNotExist:
55
+ settings_obj = None
56
+ if settings_obj and settings_obj.issuer:
57
+ return settings_obj.issuer
58
+ return original_read_str(self, key)
59
+
60
+ _core_totp_read_str._core_totp_issuer_patch = True
61
+ OTP_TOTPDevice._read_str_from_settings = _core_totp_read_str
62
+
42
63
  def create_default_arthexis(**kwargs):
43
64
  User = get_user_model()
44
65
  if not User.all_objects.exists():
@@ -765,15 +765,28 @@ class EmailInbox(Profile):
765
765
  typ, data = conn.search(None, "ALL")
766
766
  else:
767
767
  criteria = []
768
- if subject:
769
- criteria.extend(["SUBJECT", f'"{subject}"'])
770
- if from_address:
771
- criteria.extend(["FROM", f'"{from_address}"'])
772
- if body:
773
- criteria.extend(["TEXT", f'"{body}"'])
768
+ charset = None
769
+
770
+ def _append(term: str, value: str):
771
+ nonlocal charset
772
+ if not value:
773
+ return
774
+ try:
775
+ value.encode("ascii")
776
+ encoded_value = value
777
+ except UnicodeEncodeError:
778
+ charset = charset or "UTF-8"
779
+ encoded_value = value.encode("utf-8")
780
+ criteria.extend([term, encoded_value])
781
+
782
+ _append("SUBJECT", subject)
783
+ _append("FROM", from_address)
784
+ _append("TEXT", body)
785
+
774
786
  if not criteria:
775
- criteria = ["ALL"]
776
- typ, data = conn.search(None, *criteria)
787
+ typ, data = conn.search(None, "ALL")
788
+ else:
789
+ typ, data = conn.search(charset, *criteria)
777
790
  ids = data[0].split()[-fetch_limit:]
778
791
  messages = []
779
792
  for mid in ids:
@@ -2637,3 +2650,23 @@ class Todo(Entity):
2637
2650
  if isinstance(field, ConditionTextField):
2638
2651
  return field.evaluate(self)
2639
2652
  return ConditionCheckResult(True, "")
2653
+
2654
+
2655
+ class TOTPDeviceSettings(models.Model):
2656
+ """Per-device configuration options for authenticator enrollments."""
2657
+
2658
+ device = models.OneToOneField(
2659
+ "otp_totp.TOTPDevice",
2660
+ on_delete=models.CASCADE,
2661
+ related_name="custom_settings",
2662
+ )
2663
+ issuer = models.CharField(
2664
+ max_length=64,
2665
+ blank=True,
2666
+ default="",
2667
+ help_text=_("Label shown in authenticator apps. Leave blank to use Arthexis."),
2668
+ )
2669
+
2670
+ class Meta:
2671
+ verbose_name = _("Authenticator device settings")
2672
+ verbose_name_plural = _("Authenticator device settings")
@@ -30,7 +30,7 @@ def filter_visible_references(
30
30
  if host:
31
31
  site = Site.objects.filter(domain__iexact=host).first()
32
32
 
33
- site_id = site.pk if site else None
33
+ site_id = getattr(site, "pk", None)
34
34
 
35
35
  if node is None:
36
36
  try:
@@ -344,3 +344,7 @@ def publish(
344
344
  proc = subprocess.run(cmd, capture_output=True, text=True)
345
345
  if proc.returncode != 0:
346
346
  raise ReleaseError(proc.stdout + proc.stderr)
347
+
348
+ tag_name = f"v{version}"
349
+ _run(["git", "tag", tag_name])
350
+ _run(["git", "push", "origin", tag_name])