arthexis 0.1.16__tar.gz → 0.1.18__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.
Files changed (248) hide show
  1. {arthexis-0.1.16 → arthexis-0.1.18}/PKG-INFO +1 -1
  2. {arthexis-0.1.16 → arthexis-0.1.18}/arthexis.egg-info/PKG-INFO +1 -1
  3. {arthexis-0.1.16 → arthexis-0.1.18}/arthexis.egg-info/SOURCES.txt +2 -0
  4. arthexis-0.1.18/config/middleware.py +71 -0
  5. {arthexis-0.1.16 → arthexis-0.1.18}/config/settings.py +4 -0
  6. {arthexis-0.1.16 → arthexis-0.1.18}/config/urls.py +5 -0
  7. {arthexis-0.1.16 → arthexis-0.1.18}/core/admin.py +69 -9
  8. {arthexis-0.1.16 → arthexis-0.1.18}/core/backends.py +2 -0
  9. {arthexis-0.1.16 → arthexis-0.1.18}/core/changelog.py +66 -5
  10. {arthexis-0.1.16 → arthexis-0.1.18}/core/models.py +88 -7
  11. {arthexis-0.1.16 → arthexis-0.1.18}/core/release.py +55 -2
  12. {arthexis-0.1.16 → arthexis-0.1.18}/core/system.py +1 -1
  13. {arthexis-0.1.16 → arthexis-0.1.18}/core/tasks.py +0 -6
  14. {arthexis-0.1.16 → arthexis-0.1.18}/core/tests.py +131 -0
  15. {arthexis-0.1.16 → arthexis-0.1.18}/core/views.py +112 -24
  16. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/admin.py +92 -10
  17. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/consumers.py +63 -19
  18. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/test_rfid.py +118 -3
  19. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/tests.py +225 -0
  20. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/views.py +46 -7
  21. {arthexis-0.1.16 → arthexis-0.1.18}/pages/admin.py +87 -5
  22. {arthexis-0.1.16 → arthexis-0.1.18}/pages/apps.py +3 -0
  23. arthexis-0.1.18/pages/site_config.py +137 -0
  24. {arthexis-0.1.16 → arthexis-0.1.18}/pages/tests.py +206 -2
  25. {arthexis-0.1.16 → arthexis-0.1.18}/pyproject.toml +1 -1
  26. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_changelog_builder.py +124 -1
  27. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_pypi_token.py +9 -4
  28. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_progress.py +80 -9
  29. arthexis-0.1.18/tests/test_render_nginx_sites.py +72 -0
  30. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_admin_scan_csrf.py +67 -0
  31. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_backend.py +33 -0
  32. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_seed_data.py +61 -0
  33. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_temp_passwords.py +30 -1
  34. arthexis-0.1.16/config/middleware.py +0 -25
  35. {arthexis-0.1.16 → arthexis-0.1.18}/LICENSE +0 -0
  36. {arthexis-0.1.16 → arthexis-0.1.18}/README.md +0 -0
  37. {arthexis-0.1.16 → arthexis-0.1.18}/arthexis.egg-info/dependency_links.txt +0 -0
  38. {arthexis-0.1.16 → arthexis-0.1.18}/arthexis.egg-info/requires.txt +0 -0
  39. {arthexis-0.1.16 → arthexis-0.1.18}/arthexis.egg-info/top_level.txt +0 -0
  40. {arthexis-0.1.16 → arthexis-0.1.18}/config/__init__.py +0 -0
  41. {arthexis-0.1.16 → arthexis-0.1.18}/config/active_app.py +0 -0
  42. {arthexis-0.1.16 → arthexis-0.1.18}/config/asgi.py +0 -0
  43. {arthexis-0.1.16 → arthexis-0.1.18}/config/auth_app.py +0 -0
  44. {arthexis-0.1.16 → arthexis-0.1.18}/config/celery.py +0 -0
  45. {arthexis-0.1.16 → arthexis-0.1.18}/config/context_processors.py +0 -0
  46. {arthexis-0.1.16 → arthexis-0.1.18}/config/horologia_app.py +0 -0
  47. {arthexis-0.1.16 → arthexis-0.1.18}/config/loadenv.py +0 -0
  48. {arthexis-0.1.16 → arthexis-0.1.18}/config/logging.py +0 -0
  49. {arthexis-0.1.16 → arthexis-0.1.18}/config/offline.py +0 -0
  50. {arthexis-0.1.16 → arthexis-0.1.18}/config/settings_helpers.py +0 -0
  51. {arthexis-0.1.16 → arthexis-0.1.18}/config/wsgi.py +0 -0
  52. {arthexis-0.1.16 → arthexis-0.1.18}/core/__init__.py +0 -0
  53. {arthexis-0.1.16 → arthexis-0.1.18}/core/admin_history.py +0 -0
  54. {arthexis-0.1.16 → arthexis-0.1.18}/core/admindocs.py +0 -0
  55. {arthexis-0.1.16 → arthexis-0.1.18}/core/apps.py +0 -0
  56. {arthexis-0.1.16 → arthexis-0.1.18}/core/auto_upgrade.py +0 -0
  57. {arthexis-0.1.16 → arthexis-0.1.18}/core/entity.py +0 -0
  58. {arthexis-0.1.16 → arthexis-0.1.18}/core/environment.py +0 -0
  59. {arthexis-0.1.16 → arthexis-0.1.18}/core/fields.py +0 -0
  60. {arthexis-0.1.16 → arthexis-0.1.18}/core/form_fields.py +0 -0
  61. {arthexis-0.1.16 → arthexis-0.1.18}/core/github_helper.py +0 -0
  62. {arthexis-0.1.16 → arthexis-0.1.18}/core/github_issues.py +0 -0
  63. {arthexis-0.1.16 → arthexis-0.1.18}/core/github_repos.py +0 -0
  64. {arthexis-0.1.16 → arthexis-0.1.18}/core/lcd_screen.py +0 -0
  65. {arthexis-0.1.16 → arthexis-0.1.18}/core/liveupdate.py +0 -0
  66. {arthexis-0.1.16 → arthexis-0.1.18}/core/log_paths.py +0 -0
  67. {arthexis-0.1.16 → arthexis-0.1.18}/core/mailer.py +0 -0
  68. {arthexis-0.1.16 → arthexis-0.1.18}/core/middleware.py +0 -0
  69. {arthexis-0.1.16 → arthexis-0.1.18}/core/notifications.py +0 -0
  70. {arthexis-0.1.16 → arthexis-0.1.18}/core/public_wifi.py +0 -0
  71. {arthexis-0.1.16 → arthexis-0.1.18}/core/reference_utils.py +0 -0
  72. {arthexis-0.1.16 → arthexis-0.1.18}/core/rfid_import_export.py +0 -0
  73. {arthexis-0.1.16 → arthexis-0.1.18}/core/sigil_builder.py +0 -0
  74. {arthexis-0.1.16 → arthexis-0.1.18}/core/sigil_context.py +0 -0
  75. {arthexis-0.1.16 → arthexis-0.1.18}/core/sigil_resolver.py +0 -0
  76. {arthexis-0.1.16 → arthexis-0.1.18}/core/temp_passwords.py +0 -0
  77. {arthexis-0.1.16 → arthexis-0.1.18}/core/test_system_info.py +0 -0
  78. {arthexis-0.1.16 → arthexis-0.1.18}/core/tests_liveupdate.py +0 -0
  79. {arthexis-0.1.16 → arthexis-0.1.18}/core/urls.py +0 -0
  80. {arthexis-0.1.16 → arthexis-0.1.18}/core/user_data.py +0 -0
  81. {arthexis-0.1.16 → arthexis-0.1.18}/core/widgets.py +0 -0
  82. {arthexis-0.1.16 → arthexis-0.1.18}/core/workgroup_urls.py +0 -0
  83. {arthexis-0.1.16 → arthexis-0.1.18}/core/workgroup_views.py +0 -0
  84. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/__init__.py +0 -0
  85. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/admin.py +0 -0
  86. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/apps.py +0 -0
  87. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/backends.py +0 -0
  88. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/dns.py +0 -0
  89. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/feature_checks.py +0 -0
  90. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/lcd.py +0 -0
  91. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/models.py +0 -0
  92. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/reports.py +0 -0
  93. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/rfid_sync.py +0 -0
  94. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/signals.py +0 -0
  95. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/tasks.py +0 -0
  96. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/tests.py +0 -0
  97. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/urls.py +0 -0
  98. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/utils.py +0 -0
  99. {arthexis-0.1.16 → arthexis-0.1.18}/nodes/views.py +0 -0
  100. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/__init__.py +0 -0
  101. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/apps.py +0 -0
  102. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/evcs.py +0 -0
  103. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/evcs_discovery.py +0 -0
  104. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/models.py +0 -0
  105. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/reference_utils.py +0 -0
  106. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/routing.py +0 -0
  107. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/simulator.py +0 -0
  108. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/status_display.py +0 -0
  109. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/store.py +0 -0
  110. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/tasks.py +0 -0
  111. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/test_export_import.py +0 -0
  112. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/transactions_io.py +0 -0
  113. {arthexis-0.1.16 → arthexis-0.1.18}/ocpp/urls.py +0 -0
  114. {arthexis-0.1.16 → arthexis-0.1.18}/pages/__init__.py +0 -0
  115. {arthexis-0.1.16 → arthexis-0.1.18}/pages/checks.py +0 -0
  116. {arthexis-0.1.16 → arthexis-0.1.18}/pages/context_processors.py +0 -0
  117. {arthexis-0.1.16 → arthexis-0.1.18}/pages/defaults.py +0 -0
  118. {arthexis-0.1.16 → arthexis-0.1.18}/pages/forms.py +0 -0
  119. {arthexis-0.1.16 → arthexis-0.1.18}/pages/middleware.py +0 -0
  120. {arthexis-0.1.16 → arthexis-0.1.18}/pages/models.py +0 -0
  121. {arthexis-0.1.16 → arthexis-0.1.18}/pages/module_defaults.py +0 -0
  122. {arthexis-0.1.16 → arthexis-0.1.18}/pages/tasks.py +0 -0
  123. {arthexis-0.1.16 → arthexis-0.1.18}/pages/urls.py +0 -0
  124. {arthexis-0.1.16 → arthexis-0.1.18}/pages/utils.py +0 -0
  125. {arthexis-0.1.16 → arthexis-0.1.18}/pages/views.py +0 -0
  126. {arthexis-0.1.16 → arthexis-0.1.18}/setup.cfg +0 -0
  127. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_acronym_capitalization.py +0 -0
  128. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_client_report.py +0 -0
  129. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_doc_commands.py +0 -0
  130. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_doc_model_groups.py +0 -0
  131. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_history.py +0 -0
  132. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_index_actions.py +0 -0
  133. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_model_graph.py +0 -0
  134. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_object_history.py +0 -0
  135. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_profile_link.py +0 -0
  136. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_admin_system_stop.py +0 -0
  137. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_allowed_hosts_hostname.py +0 -0
  138. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_api_login_required.py +0 -0
  139. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_assistant_data_api.py +0 -0
  140. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_assistant_profile_admin.py +0 -0
  141. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_assistant_profile_api.py +0 -0
  142. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_auto_upgrade_scheduler.py +0 -0
  143. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_awg_admin.py +0 -0
  144. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_benchmark_command.py +0 -0
  145. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_birthday_greetings.py +0 -0
  146. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_build_pypi_command.py +0 -0
  147. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_celery_no_debug.py +0 -0
  148. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_check_admin_command.py +0 -0
  149. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_check_migrations_script.py +0 -0
  150. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_check_pypi_command.py +0 -0
  151. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_clean_release_logs_command.py +0 -0
  152. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_client_report_generation.py +0 -0
  153. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_client_report_schedule.py +0 -0
  154. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_csrf_failure.py +0 -0
  155. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_csrf_origin_subnet.py +0 -0
  156. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_dist_cleanup.py +0 -0
  157. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_email_collector.py +0 -0
  158. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_email_inbox.py +0 -0
  159. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_email_inbox_admin.py +0 -0
  160. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_email_inbox_search_action.py +0 -0
  161. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_email_outbox_admin.py +0 -0
  162. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_email_profiles.py +0 -0
  163. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_email_transaction.py +0 -0
  164. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_env_refresh_clean.py +0 -0
  165. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_env_refresh_pip.py +0 -0
  166. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_env_refresh_unlink.py +0 -0
  167. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_experience_admin_group.py +0 -0
  168. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_fixture_presence.py +0 -0
  169. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_footer_no_references.py +0 -0
  170. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_footer_presence.py +0 -0
  171. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_footer_render.py +0 -0
  172. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_git_checks.py +0 -0
  173. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_github_issue_reporting.py +0 -0
  174. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_install_script.py +0 -0
  175. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_invitation_login_view.py +0 -0
  176. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_language_switch.py +0 -0
  177. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_lcd_check_command.py +0 -0
  178. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_lcd_smbus2.py +0 -0
  179. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_localhost_admin_backend.py +0 -0
  180. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_log_paths.py +0 -0
  181. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_login_view_no_site.py +0 -0
  182. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_manage_debug_flag.py +0 -0
  183. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_manuals.py +0 -0
  184. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_mcp_asgi.py +0 -0
  185. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_mcp_auto_start.py +0 -0
  186. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_mcp_process.py +0 -0
  187. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_mcp_sigil_server.py +0 -0
  188. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_mcp_sigil_server_command.py +0 -0
  189. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_message_command.py +0 -0
  190. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_migrations.py +0 -0
  191. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_model_verbose_name_capitalization.py +0 -0
  192. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_network_setup_interactive.py +0 -0
  193. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_node_info_view.py +0 -0
  194. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_notifications_fallback.py +0 -0
  195. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_notify_command.py +0 -0
  196. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_ocpp_session_lock.py +0 -0
  197. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_odoo_product.py +0 -0
  198. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_odoo_profile.py +0 -0
  199. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_odoo_profile_admin.py +0 -0
  200. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_odoo_quote_report.py +0 -0
  201. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_offline.py +0 -0
  202. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_package_admin_next_release.py +0 -0
  203. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_power_admin_group.py +0 -0
  204. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_profile_inline_deletion.py +0 -0
  205. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_pypi_check.py +0 -0
  206. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_readme_language.py +0 -0
  207. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_reference_qr_code.py +0 -0
  208. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_reference_transaction_uuid.py +0 -0
  209. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_register_site_apps_command.py +0 -0
  210. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_build.py +0 -0
  211. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_build_flow.py +0 -0
  212. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_checklist.py +0 -0
  213. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_logs.py +0 -0
  214. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_manager_admin.py +0 -0
  215. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_packages.py +0 -0
  216. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_progress_pre_release_integration.py +0 -0
  217. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_push.py +0 -0
  218. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_release_tasks.py +0 -0
  219. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_request_invite.py +0 -0
  220. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_admin_print_labels.py +0 -0
  221. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_admin_reference_clear.py +0 -0
  222. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_always_on.py +0 -0
  223. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_background_reader.py +0 -0
  224. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_client_report.py +0 -0
  225. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_rfid_watch_command.py +0 -0
  226. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_role_marker_filtering.py +0 -0
  227. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_send_invite_command.py +0 -0
  228. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_settings_helpers.py +0 -0
  229. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_shell_scripts.py +0 -0
  230. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_show_leads_command.py +0 -0
  231. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_sigil_builder.py +0 -0
  232. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_sigil_resolution.py +0 -0
  233. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_sites_utils.py +0 -0
  234. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_social_profile.py +0 -0
  235. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_staff_login_net_message.py +0 -0
  236. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_staff_required_decorator.py +0 -0
  237. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_switch_role_script.py +0 -0
  238. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_system_changelog_report.py +0 -0
  239. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_totp_admin.py +0 -0
  240. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_totp_backend.py +0 -0
  241. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_uninstall_script.py +0 -0
  242. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_update_fixtures_command.py +0 -0
  243. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_upgrade_report.py +0 -0
  244. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_urls_autodiscover.py +0 -0
  245. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_user_data_admin.py +0 -0
  246. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_version_endpoint.py +0 -0
  247. {arthexis-0.1.16 → arthexis-0.1.18}/tests/test_version_file.py +0 -0
  248. {arthexis-0.1.16 → arthexis-0.1.18}/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.16
3
+ Version: 0.1.18
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.16
3
+ Version: 0.1.18
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
@@ -107,6 +107,7 @@ pages/forms.py
107
107
  pages/middleware.py
108
108
  pages/models.py
109
109
  pages/module_defaults.py
110
+ pages/site_config.py
110
111
  pages/tasks.py
111
112
  pages/tests.py
112
113
  pages/urls.py
@@ -207,6 +208,7 @@ tests/test_release_progress.py
207
208
  tests/test_release_progress_pre_release_integration.py
208
209
  tests/test_release_push.py
209
210
  tests/test_release_tasks.py
211
+ tests/test_render_nginx_sites.py
210
212
  tests/test_request_invite.py
211
213
  tests/test_rfid_admin_print_labels.py
212
214
  tests/test_rfid_admin_reference_clear.py
@@ -0,0 +1,71 @@
1
+ import socket
2
+ from django.core.exceptions import DisallowedHost
3
+ from django.http import HttpResponsePermanentRedirect
4
+ from nodes.models import Node
5
+ from utils.sites import get_site
6
+
7
+ from .active_app import set_active_app
8
+
9
+
10
+ class ActiveAppMiddleware:
11
+ """Store the current app based on the request's site."""
12
+
13
+ def __init__(self, get_response):
14
+ self.get_response = get_response
15
+
16
+ def __call__(self, request):
17
+ site = get_site(request)
18
+ node = Node.get_local()
19
+ role_name = node.role.name if node and node.role else "Terminal"
20
+ active = site.name or role_name
21
+ set_active_app(active)
22
+ request.site = site
23
+ request.active_app = active
24
+ try:
25
+ response = self.get_response(request)
26
+ finally:
27
+ set_active_app(socket.gethostname())
28
+ return response
29
+
30
+
31
+ def _is_https_request(request) -> bool:
32
+ if request.is_secure():
33
+ return True
34
+
35
+ forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO", "")
36
+ if forwarded_proto:
37
+ candidate = forwarded_proto.split(",")[0].strip().lower()
38
+ if candidate == "https":
39
+ return True
40
+
41
+ forwarded_header = request.META.get("HTTP_FORWARDED", "")
42
+ for forwarded_part in forwarded_header.split(","):
43
+ for element in forwarded_part.split(";"):
44
+ key, _, value = element.partition("=")
45
+ if key.strip().lower() == "proto" and value.strip().strip('"').lower() == "https":
46
+ return True
47
+
48
+ return False
49
+
50
+
51
+ class SiteHttpsRedirectMiddleware:
52
+ """Redirect HTTP traffic to HTTPS for sites that require it."""
53
+
54
+ def __init__(self, get_response):
55
+ self.get_response = get_response
56
+
57
+ def __call__(self, request):
58
+ site = getattr(request, "site", None)
59
+ if site is None:
60
+ site = get_site(request)
61
+ request.site = site
62
+
63
+ if getattr(site, "require_https", False) and not _is_https_request(request):
64
+ try:
65
+ host = request.get_host()
66
+ except DisallowedHost: # pragma: no cover - defensive guard
67
+ host = request.META.get("HTTP_HOST", "")
68
+ redirect_url = f"https://{host}{request.get_full_path()}"
69
+ return HttpResponsePermanentRedirect(redirect_url)
70
+
71
+ return self.get_response(request)
@@ -390,6 +390,7 @@ MIDDLEWARE = [
390
390
  "whitenoise.middleware.WhiteNoiseMiddleware",
391
391
  "django.contrib.sessions.middleware.SessionMiddleware",
392
392
  "config.middleware.ActiveAppMiddleware",
393
+ "config.middleware.SiteHttpsRedirectMiddleware",
393
394
  "django.middleware.locale.LocaleMiddleware",
394
395
  "django.middleware.common.CommonMiddleware",
395
396
  "django.middleware.csrf.CsrfViewMiddleware",
@@ -480,6 +481,9 @@ AUTHENTICATION_BACKENDS = [
480
481
  "core.backends.RFIDBackend",
481
482
  ]
482
483
 
484
+ # Use the custom login view for all authentication redirects.
485
+ LOGIN_URL = "pages:login"
486
+
483
487
  # Issuer name used when generating otpauth URLs for authenticator apps.
484
488
  OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
485
489
 
@@ -149,6 +149,11 @@ urlpatterns = [
149
149
  core_views.odoo_quote_report,
150
150
  name="odoo-quote-report",
151
151
  ),
152
+ path(
153
+ "admin/request-temp-password/",
154
+ core_views.request_temp_password,
155
+ name="admin-request-temp-password",
156
+ ),
152
157
  path("admin/", admin.site.urls),
153
158
  path("i18n/setlang/", csrf_exempt(set_language), name="set_language"),
154
159
  path("api/", include("core.workgroup_urls")),
@@ -1309,11 +1309,11 @@ class AssistantProfileInlineForm(ProfileFormMixin, forms.ModelForm):
1309
1309
  widget=forms.PasswordInput(render_value=True),
1310
1310
  help_text="Provide a plain key to create or rotate credentials.",
1311
1311
  )
1312
- profile_fields = ("user_key", "scopes", "is_active")
1312
+ profile_fields = ("assistant_name", "user_key", "scopes", "is_active")
1313
1313
 
1314
1314
  class Meta:
1315
1315
  model = AssistantProfile
1316
- fields = ("scopes", "is_active")
1316
+ fields = ("assistant_name", "scopes", "is_active")
1317
1317
 
1318
1318
  def __init__(self, *args, **kwargs):
1319
1319
  super().__init__(*args, **kwargs)
@@ -1475,7 +1475,7 @@ PROFILE_INLINE_CONFIG = {
1475
1475
  },
1476
1476
  AssistantProfile: {
1477
1477
  "form": AssistantProfileInlineForm,
1478
- "fields": ("user_key", "scopes", "is_active"),
1478
+ "fields": ("assistant_name", "user_key", "scopes", "is_active"),
1479
1479
  "readonly_fields": ("user_key_hash", "created_at", "last_used_at"),
1480
1480
  "template": "admin/edit_inline/profile_stacked.html",
1481
1481
  },
@@ -2016,7 +2016,7 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
2016
2016
  class AssistantProfileAdmin(
2017
2017
  ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
2018
2018
  ):
2019
- list_display = ("owner", "created_at", "last_used_at", "is_active")
2019
+ list_display = ("assistant_name", "owner", "created_at", "last_used_at", "is_active")
2020
2020
  readonly_fields = ("user_key_hash", "created_at", "last_used_at")
2021
2021
 
2022
2022
  change_form_template = "admin/workgroupassistantprofile_change_form.html"
@@ -2028,7 +2028,15 @@ class AssistantProfileAdmin(
2028
2028
  ("Credentials", {"fields": ("user_key_hash",)}),
2029
2029
  (
2030
2030
  "Configuration",
2031
- {"fields": ("scopes", "is_active", "created_at", "last_used_at")},
2031
+ {
2032
+ "fields": (
2033
+ "assistant_name",
2034
+ "scopes",
2035
+ "is_active",
2036
+ "created_at",
2037
+ "last_used_at",
2038
+ )
2039
+ },
2032
2040
  ),
2033
2041
  )
2034
2042
 
@@ -2835,6 +2843,7 @@ class RFIDResource(resources.ModelResource):
2835
2843
  "post_auth_command",
2836
2844
  "allowed",
2837
2845
  "color",
2846
+ "endianness",
2838
2847
  "kind",
2839
2848
  "released",
2840
2849
  "last_seen_on",
@@ -2849,6 +2858,7 @@ class RFIDResource(resources.ModelResource):
2849
2858
  "post_auth_command",
2850
2859
  "allowed",
2851
2860
  "color",
2861
+ "endianness",
2852
2862
  "kind",
2853
2863
  "released",
2854
2864
  "last_seen_on",
@@ -2919,11 +2929,12 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2919
2929
  "user_data_flag",
2920
2930
  "color",
2921
2931
  "kind",
2932
+ "endianness",
2922
2933
  "released",
2923
2934
  "allowed",
2924
2935
  "last_seen_on",
2925
2936
  )
2926
- list_filter = ("color", "released", "allowed")
2937
+ list_filter = ("color", "endianness", "released", "allowed")
2927
2938
  search_fields = ("label_id", "rfid", "custom_label")
2928
2939
  autocomplete_fields = ["energy_accounts"]
2929
2940
  raw_id_fields = ["reference"]
@@ -2933,8 +2944,10 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2933
2944
  "print_release_form",
2934
2945
  "copy_rfids",
2935
2946
  "toggle_selected_user_data",
2947
+ "toggle_selected_released",
2948
+ "toggle_selected_allowed",
2936
2949
  ]
2937
- readonly_fields = ("added_on", "last_seen_on")
2950
+ readonly_fields = ("added_on", "last_seen_on", "reversed_uid")
2938
2951
  form = RFIDForm
2939
2952
 
2940
2953
  def get_import_resource_kwargs(self, request, form=None, **kwargs):
@@ -3030,6 +3043,50 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3030
3043
  level=messages.WARNING,
3031
3044
  )
3032
3045
 
3046
+ @admin.action(description=_("Toggle Released flag"))
3047
+ def toggle_selected_released(self, request, queryset):
3048
+ manager = getattr(self.model, "all_objects", self.model.objects)
3049
+ toggled = 0
3050
+ for tag in queryset:
3051
+ new_state = not tag.released
3052
+ manager.filter(pk=tag.pk).update(released=new_state)
3053
+ tag.released = new_state
3054
+ toggled += 1
3055
+
3056
+ if toggled:
3057
+ self.message_user(
3058
+ request,
3059
+ ngettext(
3060
+ "Toggled released flag for %(count)d RFID.",
3061
+ "Toggled released flag for %(count)d RFIDs.",
3062
+ toggled,
3063
+ )
3064
+ % {"count": toggled},
3065
+ level=messages.SUCCESS,
3066
+ )
3067
+
3068
+ @admin.action(description=_("Toggle Allowed flag"))
3069
+ def toggle_selected_allowed(self, request, queryset):
3070
+ manager = getattr(self.model, "all_objects", self.model.objects)
3071
+ toggled = 0
3072
+ for tag in queryset:
3073
+ new_state = not tag.allowed
3074
+ manager.filter(pk=tag.pk).update(allowed=new_state)
3075
+ tag.allowed = new_state
3076
+ toggled += 1
3077
+
3078
+ if toggled:
3079
+ self.message_user(
3080
+ request,
3081
+ ngettext(
3082
+ "Toggled allowed flag for %(count)d RFID.",
3083
+ "Toggled allowed flag for %(count)d RFIDs.",
3084
+ toggled,
3085
+ )
3086
+ % {"count": toggled},
3087
+ level=messages.SUCCESS,
3088
+ )
3089
+
3033
3090
  @admin.action(description=_("Copy RFID"))
3034
3091
  def copy_rfids(self, request, queryset):
3035
3092
  if queryset.count() != 1:
@@ -3497,6 +3554,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3497
3554
  context["title"] = _("Scan RFIDs")
3498
3555
  context["opts"] = self.model._meta
3499
3556
  context["show_release_info"] = True
3557
+ context["default_endianness"] = RFID.BIG_ENDIAN
3500
3558
  return render(request, "admin/core/rfid/scan.html", context)
3501
3559
 
3502
3560
  def scan_next(self, request):
@@ -3510,9 +3568,11 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3510
3568
  return JsonResponse({"error": "Invalid JSON payload"}, status=400)
3511
3569
  rfid = payload.get("rfid") or payload.get("value")
3512
3570
  kind = payload.get("kind")
3513
- result = validate_rfid_value(rfid, kind=kind)
3571
+ endianness = payload.get("endianness")
3572
+ result = validate_rfid_value(rfid, kind=kind, endianness=endianness)
3514
3573
  else:
3515
- result = scan_sources(request)
3574
+ endianness = request.GET.get("endianness")
3575
+ result = scan_sources(request, endianness=endianness)
3516
3576
  status = 500 if result.get("error") else 200
3517
3577
  return JsonResponse(result, status=status)
3518
3578
 
@@ -90,6 +90,7 @@ class RFIDBackend:
90
90
  env = os.environ.copy()
91
91
  env["RFID_VALUE"] = rfid_value
92
92
  env["RFID_LABEL_ID"] = str(tag.pk)
93
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
93
94
  try:
94
95
  completed = subprocess.run(
95
96
  command,
@@ -117,6 +118,7 @@ class RFIDBackend:
117
118
  env = os.environ.copy()
118
119
  env["RFID_VALUE"] = rfid_value
119
120
  env["RFID_LABEL_ID"] = str(tag.pk)
121
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
120
122
  with contextlib.suppress(Exception):
121
123
  subprocess.Popen(
122
124
  post_command,
@@ -154,9 +154,53 @@ def _parse_sections(text: str) -> List[ChangelogSection]:
154
154
  return sections
155
155
 
156
156
 
157
+ def _latest_release_version(previous_text: str) -> Optional[str]:
158
+ for section in _parse_sections(previous_text):
159
+ if section.version:
160
+ return section.version
161
+ return None
162
+
163
+
164
+ def _find_release_commit(version: str) -> Optional[str]:
165
+ normalized = version.lstrip("v")
166
+ search_terms = [
167
+ f"Release v{normalized}",
168
+ f"Release {normalized}",
169
+ f"pre-release commit v{normalized}",
170
+ f"pre-release commit {normalized}",
171
+ ]
172
+ for term in search_terms:
173
+ proc = subprocess.run(
174
+ [
175
+ "git",
176
+ "log",
177
+ "--max-count=1",
178
+ "--format=%H",
179
+ "--fixed-strings",
180
+ f"--grep={term}",
181
+ ],
182
+ capture_output=True,
183
+ text=True,
184
+ check=False,
185
+ )
186
+ sha = proc.stdout.strip()
187
+ if sha:
188
+ return sha.splitlines()[0]
189
+ return None
190
+
191
+
192
+ def _resolve_release_commit_from_text(previous_text: str) -> Optional[str]:
193
+ version = _latest_release_version(previous_text)
194
+ if not version:
195
+ return None
196
+ return _find_release_commit(version)
197
+
198
+
157
199
  def _merge_sections(
158
200
  new_sections: Iterable[ChangelogSection],
159
201
  old_sections: Iterable[ChangelogSection],
202
+ *,
203
+ reopen_latest: bool = False,
160
204
  ) -> List[ChangelogSection]:
161
205
  merged = list(new_sections)
162
206
  old_sections_list = list(old_sections)
@@ -199,7 +243,8 @@ def _merge_sections(
199
243
  existing = version_to_section.get(old.version)
200
244
  if existing is None:
201
245
  if (
202
- first_release_version
246
+ reopen_latest
247
+ and first_release_version
203
248
  and old.version == first_release_version
204
249
  and not reopened_latest_version
205
250
  and unreleased_section is not None
@@ -274,29 +319,45 @@ def _resolve_start_tag(explicit: str | None = None) -> Optional[str]:
274
319
  return None
275
320
 
276
321
 
277
- def determine_range_spec(start_tag: str | None = None) -> str:
322
+ def determine_range_spec(
323
+ start_tag: str | None = None, *, previous_text: str | None = None
324
+ ) -> str:
278
325
  """Return the git range specification to build the changelog."""
279
326
 
280
327
  resolved = _resolve_start_tag(start_tag)
281
328
  if resolved:
282
329
  return f"{resolved}..HEAD"
330
+
331
+ if previous_text:
332
+ release_commit = _resolve_release_commit_from_text(previous_text)
333
+ if release_commit:
334
+ return f"{release_commit}..HEAD"
335
+
283
336
  return "HEAD"
284
337
 
285
338
 
286
339
  def collect_sections(
287
- *, range_spec: str = "HEAD", previous_text: str | None = None
340
+ *,
341
+ range_spec: str = "HEAD",
342
+ previous_text: str | None = None,
343
+ reopen_latest: bool = False,
288
344
  ) -> List[ChangelogSection]:
289
345
  """Return changelog sections for *range_spec*.
290
346
 
291
347
  When ``previous_text`` is provided, sections not regenerated in the current run
292
- are appended so long as they can be parsed from the existing changelog.
348
+ are appended so long as they can be parsed from the existing changelog. Set
349
+ ``reopen_latest`` to ``True`` when the caller intends to move the most recent
350
+ release notes back into the ``Unreleased`` section (for example, when
351
+ preparing a release retry before a new tag is created).
293
352
  """
294
353
 
295
354
  commits = _read_commits(range_spec)
296
355
  sections = _sections_from_commits(commits)
297
356
  if previous_text:
298
357
  old_sections = _parse_sections(previous_text)
299
- sections = _merge_sections(sections, old_sections)
358
+ sections = _merge_sections(
359
+ sections, old_sections, reopen_latest=reopen_latest
360
+ )
300
361
  return sections
301
362
 
302
363
 
@@ -1775,6 +1775,14 @@ class RFID(Entity):
1775
1775
  )
1776
1776
  ],
1777
1777
  )
1778
+ reversed_uid = models.CharField(
1779
+ max_length=255,
1780
+ default="",
1781
+ blank=True,
1782
+ editable=False,
1783
+ verbose_name="Reversed UID",
1784
+ help_text="UID value stored with opposite endianness for reference.",
1785
+ )
1778
1786
  custom_label = models.CharField(
1779
1787
  max_length=32,
1780
1788
  blank=True,
@@ -1851,6 +1859,17 @@ class RFID(Entity):
1851
1859
  choices=KIND_CHOICES,
1852
1860
  default=CLASSIC,
1853
1861
  )
1862
+ BIG_ENDIAN = "BIG"
1863
+ LITTLE_ENDIAN = "LITTLE"
1864
+ ENDIANNESS_CHOICES = [
1865
+ (BIG_ENDIAN, _("Big endian")),
1866
+ (LITTLE_ENDIAN, _("Little endian")),
1867
+ ]
1868
+ endianness = models.CharField(
1869
+ max_length=6,
1870
+ choices=ENDIANNESS_CHOICES,
1871
+ default=BIG_ENDIAN,
1872
+ )
1854
1873
  reference = models.ForeignKey(
1855
1874
  "Reference",
1856
1875
  null=True,
@@ -1895,13 +1914,24 @@ class RFID(Entity):
1895
1914
  if self.key_b and old["key_b"] != self.key_b.upper():
1896
1915
  self.key_b_verified = False
1897
1916
  if self.rfid:
1898
- self.rfid = self.rfid.upper()
1917
+ normalized_rfid = self.rfid.upper()
1918
+ self.rfid = normalized_rfid
1919
+ reversed_uid = self.reverse_uid(normalized_rfid)
1920
+ if reversed_uid != self.reversed_uid:
1921
+ self.reversed_uid = reversed_uid
1922
+ if update_fields:
1923
+ fields = set(update_fields)
1924
+ if "reversed_uid" not in fields:
1925
+ fields.add("reversed_uid")
1926
+ kwargs["update_fields"] = tuple(fields)
1899
1927
  if self.key_a:
1900
1928
  self.key_a = self.key_a.upper()
1901
1929
  if self.key_b:
1902
1930
  self.key_b = self.key_b.upper()
1903
1931
  if self.kind:
1904
1932
  self.kind = self.kind.upper()
1933
+ if self.endianness:
1934
+ self.endianness = self.normalize_endianness(self.endianness)
1905
1935
  super().save(*args, **kwargs)
1906
1936
  if not self.allowed:
1907
1937
  self.energy_accounts.clear()
@@ -1909,6 +1939,30 @@ class RFID(Entity):
1909
1939
  def __str__(self): # pragma: no cover - simple representation
1910
1940
  return str(self.label_id)
1911
1941
 
1942
+ @classmethod
1943
+ def normalize_endianness(cls, value: object) -> str:
1944
+ """Return a valid endianness value, defaulting to BIG."""
1945
+
1946
+ if isinstance(value, str):
1947
+ candidate = value.strip().upper()
1948
+ valid = {choice[0] for choice in cls.ENDIANNESS_CHOICES}
1949
+ if candidate in valid:
1950
+ return candidate
1951
+ return cls.BIG_ENDIAN
1952
+
1953
+ @staticmethod
1954
+ def reverse_uid(value: str) -> str:
1955
+ """Return ``value`` with reversed byte order for reference storage."""
1956
+
1957
+ normalized = "".join((value or "").split()).upper()
1958
+ if not normalized:
1959
+ return ""
1960
+ if len(normalized) % 2 != 0:
1961
+ return normalized[::-1]
1962
+ bytes_list = [normalized[index : index + 2] for index in range(0, len(normalized), 2)]
1963
+ bytes_list.reverse()
1964
+ return "".join(bytes_list)
1965
+
1912
1966
  @classmethod
1913
1967
  def next_scan_label(
1914
1968
  cls, *, step: int | None = None, start: int | None = None
@@ -1971,13 +2025,39 @@ class RFID(Entity):
1971
2025
 
1972
2026
  @classmethod
1973
2027
  def register_scan(
1974
- cls, rfid: str, *, kind: str | None = None
2028
+ cls,
2029
+ rfid: str,
2030
+ *,
2031
+ kind: str | None = None,
2032
+ endianness: str | None = None,
1975
2033
  ) -> tuple["RFID", bool]:
1976
2034
  """Return or create an RFID that was detected via scanning."""
1977
2035
 
1978
- normalized = (rfid or "").upper()
1979
- existing = cls.objects.filter(rfid=normalized).first()
2036
+ normalized = "".join((rfid or "").split()).upper()
2037
+ desired_endianness = cls.normalize_endianness(endianness)
2038
+ alternate = None
2039
+ if normalized and len(normalized) % 2 == 0:
2040
+ bytes_list = [normalized[i : i + 2] for i in range(0, len(normalized), 2)]
2041
+ bytes_list.reverse()
2042
+ alternate_candidate = "".join(bytes_list)
2043
+ if alternate_candidate != normalized:
2044
+ alternate = alternate_candidate
2045
+
2046
+ existing = None
2047
+ if normalized:
2048
+ existing = cls.objects.filter(rfid=normalized).first()
2049
+ if not existing and alternate:
2050
+ existing = cls.objects.filter(rfid=alternate).first()
1980
2051
  if existing:
2052
+ update_fields: list[str] = []
2053
+ if normalized and existing.rfid != normalized:
2054
+ existing.rfid = normalized
2055
+ update_fields.append("rfid")
2056
+ if existing.endianness != desired_endianness:
2057
+ existing.endianness = desired_endianness
2058
+ update_fields.append("endianness")
2059
+ if update_fields:
2060
+ existing.save(update_fields=update_fields)
1981
2061
  return existing, False
1982
2062
 
1983
2063
  attempts = 0
@@ -1990,6 +2070,7 @@ class RFID(Entity):
1990
2070
  "rfid": normalized,
1991
2071
  "allowed": True,
1992
2072
  "released": False,
2073
+ "endianness": desired_endianness,
1993
2074
  }
1994
2075
  if kind:
1995
2076
  create_kwargs["kind"] = kind
@@ -3539,7 +3620,8 @@ class AssistantProfile(Profile):
3539
3620
  """
3540
3621
 
3541
3622
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
3542
- profile_fields = ("user_key_hash", "scopes", "is_active")
3623
+ profile_fields = ("assistant_name", "user_key_hash", "scopes", "is_active")
3624
+ assistant_name = models.CharField(max_length=100, default="Assistant")
3543
3625
  user_key_hash = models.CharField(max_length=64, unique=True)
3544
3626
  scopes = models.JSONField(default=list, blank=True)
3545
3627
  created_at = models.DateTimeField(auto_now_add=True)
@@ -3586,8 +3668,7 @@ class AssistantProfile(Profile):
3586
3668
  self.save(update_fields=["last_used_at"])
3587
3669
 
3588
3670
  def __str__(self) -> str: # pragma: no cover - simple representation
3589
- owner = self.owner_display()
3590
- return f"AssistantProfile for {owner}" if owner else "AssistantProfile"
3671
+ return self.assistant_name or "AssistantProfile"
3591
3672
 
3592
3673
 
3593
3674
  def validate_relative_url(value: str) -> None:
@@ -114,6 +114,21 @@ class ReleaseError(Exception):
114
114
  pass
115
115
 
116
116
 
117
+ class PostPublishWarning(ReleaseError):
118
+ """Raised when distribution uploads succeed but post-publish tasks need attention."""
119
+
120
+ def __init__(
121
+ self,
122
+ message: str,
123
+ *,
124
+ uploaded: Sequence[str],
125
+ followups: Optional[Sequence[str]] = None,
126
+ ) -> None:
127
+ super().__init__(message)
128
+ self.uploaded = list(uploaded)
129
+ self.followups = list(followups or [])
130
+
131
+
117
132
  class TestsFailed(ReleaseError):
118
133
  """Raised when the test suite fails.
119
134
 
@@ -711,8 +726,46 @@ def publish(
711
726
  uploaded.append(target.name)
712
727
 
713
728
  tag_name = f"v{version}"
714
- _run(["git", "tag", tag_name])
715
- _push_tag(tag_name, package)
729
+ try:
730
+ _run(["git", "tag", tag_name])
731
+ except subprocess.CalledProcessError as exc:
732
+ details = _format_subprocess_error(exc)
733
+ if uploaded:
734
+ uploads = ", ".join(uploaded)
735
+ if details:
736
+ message = (
737
+ f"Upload to {uploads} completed, but creating git tag {tag_name} failed: {details}"
738
+ )
739
+ else:
740
+ message = (
741
+ f"Upload to {uploads} completed, but creating git tag {tag_name} failed."
742
+ )
743
+ followups = [f"Create and push git tag {tag_name} manually once the repository is ready."]
744
+ raise PostPublishWarning(
745
+ message,
746
+ uploaded=uploaded,
747
+ followups=followups,
748
+ ) from exc
749
+ raise ReleaseError(
750
+ f"Failed to create git tag {tag_name}: {details or exc}"
751
+ ) from exc
752
+
753
+ try:
754
+ _push_tag(tag_name, package)
755
+ except ReleaseError as exc:
756
+ if uploaded:
757
+ uploads = ", ".join(uploaded)
758
+ message = f"Upload to {uploads} completed, but {exc}"
759
+ followups = [
760
+ f"Push git tag {tag_name} to origin after resolving the reported issue."
761
+ ]
762
+ warning = PostPublishWarning(
763
+ message,
764
+ uploaded=uploaded,
765
+ followups=followups,
766
+ )
767
+ raise warning from exc
768
+ raise
716
769
  return uploaded
717
770
 
718
771
 
@@ -171,7 +171,7 @@ def _regenerate_changelog() -> None:
171
171
  previous_text = (
172
172
  changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
173
173
  )
174
- range_spec = changelog_utils.determine_range_spec()
174
+ range_spec = changelog_utils.determine_range_spec(previous_text=previous_text)
175
175
  sections = changelog_utils.collect_sections(
176
176
  range_spec=range_spec, previous_text=previous_text
177
177
  )
@@ -230,12 +230,6 @@ def check_github_updates() -> None:
230
230
 
231
231
  subprocess.run(args, cwd=base_dir, check=True)
232
232
 
233
- if shutil.which("gway"):
234
- try:
235
- subprocess.run(["gway", "upgrade"], check=True)
236
- except subprocess.CalledProcessError:
237
- logger.warning("gway upgrade failed; continuing anyway", exc_info=True)
238
-
239
233
  service_file = base_dir / "locks/service.lck"
240
234
  if service_file.exists():
241
235
  service = service_file.read_text().strip()