arthexis 0.1.17__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.17 → arthexis-0.1.18}/PKG-INFO +1 -1
  2. {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/PKG-INFO +1 -1
  3. {arthexis-0.1.17 → 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.17 → arthexis-0.1.18}/config/settings.py +1 -0
  6. {arthexis-0.1.17 → arthexis-0.1.18}/config/urls.py +5 -0
  7. {arthexis-0.1.17 → arthexis-0.1.18}/core/admin.py +1 -1
  8. {arthexis-0.1.17 → arthexis-0.1.18}/core/models.py +31 -1
  9. {arthexis-0.1.17 → arthexis-0.1.18}/core/tests.py +9 -0
  10. {arthexis-0.1.17 → arthexis-0.1.18}/core/views.py +55 -18
  11. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/consumers.py +63 -19
  12. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/test_rfid.py +70 -0
  13. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/tests.py +93 -0
  14. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/views.py +23 -2
  15. {arthexis-0.1.17 → arthexis-0.1.18}/pages/admin.py +87 -5
  16. {arthexis-0.1.17 → arthexis-0.1.18}/pages/apps.py +3 -0
  17. arthexis-0.1.18/pages/site_config.py +137 -0
  18. {arthexis-0.1.17 → arthexis-0.1.18}/pages/tests.py +180 -1
  19. {arthexis-0.1.17 → arthexis-0.1.18}/pyproject.toml +1 -1
  20. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_progress.py +13 -0
  21. arthexis-0.1.18/tests/test_render_nginx_sites.py +72 -0
  22. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_seed_data.py +61 -0
  23. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_temp_passwords.py +30 -1
  24. arthexis-0.1.17/config/middleware.py +0 -25
  25. {arthexis-0.1.17 → arthexis-0.1.18}/LICENSE +0 -0
  26. {arthexis-0.1.17 → arthexis-0.1.18}/README.md +0 -0
  27. {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/dependency_links.txt +0 -0
  28. {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/requires.txt +0 -0
  29. {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/top_level.txt +0 -0
  30. {arthexis-0.1.17 → arthexis-0.1.18}/config/__init__.py +0 -0
  31. {arthexis-0.1.17 → arthexis-0.1.18}/config/active_app.py +0 -0
  32. {arthexis-0.1.17 → arthexis-0.1.18}/config/asgi.py +0 -0
  33. {arthexis-0.1.17 → arthexis-0.1.18}/config/auth_app.py +0 -0
  34. {arthexis-0.1.17 → arthexis-0.1.18}/config/celery.py +0 -0
  35. {arthexis-0.1.17 → arthexis-0.1.18}/config/context_processors.py +0 -0
  36. {arthexis-0.1.17 → arthexis-0.1.18}/config/horologia_app.py +0 -0
  37. {arthexis-0.1.17 → arthexis-0.1.18}/config/loadenv.py +0 -0
  38. {arthexis-0.1.17 → arthexis-0.1.18}/config/logging.py +0 -0
  39. {arthexis-0.1.17 → arthexis-0.1.18}/config/offline.py +0 -0
  40. {arthexis-0.1.17 → arthexis-0.1.18}/config/settings_helpers.py +0 -0
  41. {arthexis-0.1.17 → arthexis-0.1.18}/config/wsgi.py +0 -0
  42. {arthexis-0.1.17 → arthexis-0.1.18}/core/__init__.py +0 -0
  43. {arthexis-0.1.17 → arthexis-0.1.18}/core/admin_history.py +0 -0
  44. {arthexis-0.1.17 → arthexis-0.1.18}/core/admindocs.py +0 -0
  45. {arthexis-0.1.17 → arthexis-0.1.18}/core/apps.py +0 -0
  46. {arthexis-0.1.17 → arthexis-0.1.18}/core/auto_upgrade.py +0 -0
  47. {arthexis-0.1.17 → arthexis-0.1.18}/core/backends.py +0 -0
  48. {arthexis-0.1.17 → arthexis-0.1.18}/core/changelog.py +0 -0
  49. {arthexis-0.1.17 → arthexis-0.1.18}/core/entity.py +0 -0
  50. {arthexis-0.1.17 → arthexis-0.1.18}/core/environment.py +0 -0
  51. {arthexis-0.1.17 → arthexis-0.1.18}/core/fields.py +0 -0
  52. {arthexis-0.1.17 → arthexis-0.1.18}/core/form_fields.py +0 -0
  53. {arthexis-0.1.17 → arthexis-0.1.18}/core/github_helper.py +0 -0
  54. {arthexis-0.1.17 → arthexis-0.1.18}/core/github_issues.py +0 -0
  55. {arthexis-0.1.17 → arthexis-0.1.18}/core/github_repos.py +0 -0
  56. {arthexis-0.1.17 → arthexis-0.1.18}/core/lcd_screen.py +0 -0
  57. {arthexis-0.1.17 → arthexis-0.1.18}/core/liveupdate.py +0 -0
  58. {arthexis-0.1.17 → arthexis-0.1.18}/core/log_paths.py +0 -0
  59. {arthexis-0.1.17 → arthexis-0.1.18}/core/mailer.py +0 -0
  60. {arthexis-0.1.17 → arthexis-0.1.18}/core/middleware.py +0 -0
  61. {arthexis-0.1.17 → arthexis-0.1.18}/core/notifications.py +0 -0
  62. {arthexis-0.1.17 → arthexis-0.1.18}/core/public_wifi.py +0 -0
  63. {arthexis-0.1.17 → arthexis-0.1.18}/core/reference_utils.py +0 -0
  64. {arthexis-0.1.17 → arthexis-0.1.18}/core/release.py +0 -0
  65. {arthexis-0.1.17 → arthexis-0.1.18}/core/rfid_import_export.py +0 -0
  66. {arthexis-0.1.17 → arthexis-0.1.18}/core/sigil_builder.py +0 -0
  67. {arthexis-0.1.17 → arthexis-0.1.18}/core/sigil_context.py +0 -0
  68. {arthexis-0.1.17 → arthexis-0.1.18}/core/sigil_resolver.py +0 -0
  69. {arthexis-0.1.17 → arthexis-0.1.18}/core/system.py +0 -0
  70. {arthexis-0.1.17 → arthexis-0.1.18}/core/tasks.py +0 -0
  71. {arthexis-0.1.17 → arthexis-0.1.18}/core/temp_passwords.py +0 -0
  72. {arthexis-0.1.17 → arthexis-0.1.18}/core/test_system_info.py +0 -0
  73. {arthexis-0.1.17 → arthexis-0.1.18}/core/tests_liveupdate.py +0 -0
  74. {arthexis-0.1.17 → arthexis-0.1.18}/core/urls.py +0 -0
  75. {arthexis-0.1.17 → arthexis-0.1.18}/core/user_data.py +0 -0
  76. {arthexis-0.1.17 → arthexis-0.1.18}/core/widgets.py +0 -0
  77. {arthexis-0.1.17 → arthexis-0.1.18}/core/workgroup_urls.py +0 -0
  78. {arthexis-0.1.17 → arthexis-0.1.18}/core/workgroup_views.py +0 -0
  79. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/__init__.py +0 -0
  80. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/admin.py +0 -0
  81. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/apps.py +0 -0
  82. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/backends.py +0 -0
  83. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/dns.py +0 -0
  84. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/feature_checks.py +0 -0
  85. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/lcd.py +0 -0
  86. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/models.py +0 -0
  87. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/reports.py +0 -0
  88. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/rfid_sync.py +0 -0
  89. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/signals.py +0 -0
  90. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/tasks.py +0 -0
  91. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/tests.py +0 -0
  92. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/urls.py +0 -0
  93. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/utils.py +0 -0
  94. {arthexis-0.1.17 → arthexis-0.1.18}/nodes/views.py +0 -0
  95. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/__init__.py +0 -0
  96. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/admin.py +0 -0
  97. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/apps.py +0 -0
  98. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/evcs.py +0 -0
  99. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/evcs_discovery.py +0 -0
  100. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/models.py +0 -0
  101. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/reference_utils.py +0 -0
  102. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/routing.py +0 -0
  103. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/simulator.py +0 -0
  104. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/status_display.py +0 -0
  105. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/store.py +0 -0
  106. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/tasks.py +0 -0
  107. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/test_export_import.py +0 -0
  108. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/transactions_io.py +0 -0
  109. {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/urls.py +0 -0
  110. {arthexis-0.1.17 → arthexis-0.1.18}/pages/__init__.py +0 -0
  111. {arthexis-0.1.17 → arthexis-0.1.18}/pages/checks.py +0 -0
  112. {arthexis-0.1.17 → arthexis-0.1.18}/pages/context_processors.py +0 -0
  113. {arthexis-0.1.17 → arthexis-0.1.18}/pages/defaults.py +0 -0
  114. {arthexis-0.1.17 → arthexis-0.1.18}/pages/forms.py +0 -0
  115. {arthexis-0.1.17 → arthexis-0.1.18}/pages/middleware.py +0 -0
  116. {arthexis-0.1.17 → arthexis-0.1.18}/pages/models.py +0 -0
  117. {arthexis-0.1.17 → arthexis-0.1.18}/pages/module_defaults.py +0 -0
  118. {arthexis-0.1.17 → arthexis-0.1.18}/pages/tasks.py +0 -0
  119. {arthexis-0.1.17 → arthexis-0.1.18}/pages/urls.py +0 -0
  120. {arthexis-0.1.17 → arthexis-0.1.18}/pages/utils.py +0 -0
  121. {arthexis-0.1.17 → arthexis-0.1.18}/pages/views.py +0 -0
  122. {arthexis-0.1.17 → arthexis-0.1.18}/setup.cfg +0 -0
  123. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_acronym_capitalization.py +0 -0
  124. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_client_report.py +0 -0
  125. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_doc_commands.py +0 -0
  126. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_doc_model_groups.py +0 -0
  127. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_history.py +0 -0
  128. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_index_actions.py +0 -0
  129. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_model_graph.py +0 -0
  130. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_object_history.py +0 -0
  131. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_profile_link.py +0 -0
  132. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_system_stop.py +0 -0
  133. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_allowed_hosts_hostname.py +0 -0
  134. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_api_login_required.py +0 -0
  135. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_assistant_data_api.py +0 -0
  136. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_assistant_profile_admin.py +0 -0
  137. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_assistant_profile_api.py +0 -0
  138. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_auto_upgrade_scheduler.py +0 -0
  139. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_awg_admin.py +0 -0
  140. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_benchmark_command.py +0 -0
  141. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_birthday_greetings.py +0 -0
  142. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_build_pypi_command.py +0 -0
  143. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_celery_no_debug.py +0 -0
  144. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_changelog_builder.py +0 -0
  145. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_check_admin_command.py +0 -0
  146. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_check_migrations_script.py +0 -0
  147. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_check_pypi_command.py +0 -0
  148. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_clean_release_logs_command.py +0 -0
  149. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_client_report_generation.py +0 -0
  150. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_client_report_schedule.py +0 -0
  151. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_csrf_failure.py +0 -0
  152. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_csrf_origin_subnet.py +0 -0
  153. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_dist_cleanup.py +0 -0
  154. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_collector.py +0 -0
  155. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_inbox.py +0 -0
  156. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_inbox_admin.py +0 -0
  157. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_inbox_search_action.py +0 -0
  158. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_outbox_admin.py +0 -0
  159. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_profiles.py +0 -0
  160. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_transaction.py +0 -0
  161. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_env_refresh_clean.py +0 -0
  162. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_env_refresh_pip.py +0 -0
  163. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_env_refresh_unlink.py +0 -0
  164. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_experience_admin_group.py +0 -0
  165. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_fixture_presence.py +0 -0
  166. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_footer_no_references.py +0 -0
  167. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_footer_presence.py +0 -0
  168. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_footer_render.py +0 -0
  169. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_git_checks.py +0 -0
  170. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_github_issue_reporting.py +0 -0
  171. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_install_script.py +0 -0
  172. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_invitation_login_view.py +0 -0
  173. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_language_switch.py +0 -0
  174. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_lcd_check_command.py +0 -0
  175. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_lcd_smbus2.py +0 -0
  176. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_localhost_admin_backend.py +0 -0
  177. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_log_paths.py +0 -0
  178. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_login_view_no_site.py +0 -0
  179. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_manage_debug_flag.py +0 -0
  180. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_manuals.py +0 -0
  181. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_asgi.py +0 -0
  182. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_auto_start.py +0 -0
  183. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_process.py +0 -0
  184. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_sigil_server.py +0 -0
  185. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_sigil_server_command.py +0 -0
  186. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_message_command.py +0 -0
  187. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_migrations.py +0 -0
  188. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_model_verbose_name_capitalization.py +0 -0
  189. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_network_setup_interactive.py +0 -0
  190. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_node_info_view.py +0 -0
  191. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_notifications_fallback.py +0 -0
  192. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_notify_command.py +0 -0
  193. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_ocpp_session_lock.py +0 -0
  194. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_product.py +0 -0
  195. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_profile.py +0 -0
  196. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_profile_admin.py +0 -0
  197. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_quote_report.py +0 -0
  198. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_offline.py +0 -0
  199. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_package_admin_next_release.py +0 -0
  200. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_power_admin_group.py +0 -0
  201. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_profile_inline_deletion.py +0 -0
  202. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_pypi_check.py +0 -0
  203. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_pypi_token.py +0 -0
  204. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_readme_language.py +0 -0
  205. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_reference_qr_code.py +0 -0
  206. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_reference_transaction_uuid.py +0 -0
  207. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_register_site_apps_command.py +0 -0
  208. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_build.py +0 -0
  209. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_build_flow.py +0 -0
  210. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_checklist.py +0 -0
  211. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_logs.py +0 -0
  212. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_manager_admin.py +0 -0
  213. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_packages.py +0 -0
  214. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_progress_pre_release_integration.py +0 -0
  215. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_push.py +0 -0
  216. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_tasks.py +0 -0
  217. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_request_invite.py +0 -0
  218. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_admin_print_labels.py +0 -0
  219. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_admin_reference_clear.py +0 -0
  220. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_admin_scan_csrf.py +0 -0
  221. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_always_on.py +0 -0
  222. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_backend.py +0 -0
  223. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_background_reader.py +0 -0
  224. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_client_report.py +0 -0
  225. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_watch_command.py +0 -0
  226. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_role_marker_filtering.py +0 -0
  227. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_send_invite_command.py +0 -0
  228. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_settings_helpers.py +0 -0
  229. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_shell_scripts.py +0 -0
  230. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_show_leads_command.py +0 -0
  231. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_sigil_builder.py +0 -0
  232. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_sigil_resolution.py +0 -0
  233. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_sites_utils.py +0 -0
  234. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_social_profile.py +0 -0
  235. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_staff_login_net_message.py +0 -0
  236. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_staff_required_decorator.py +0 -0
  237. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_switch_role_script.py +0 -0
  238. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_system_changelog_report.py +0 -0
  239. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_totp_admin.py +0 -0
  240. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_totp_backend.py +0 -0
  241. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_uninstall_script.py +0 -0
  242. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_update_fixtures_command.py +0 -0
  243. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_upgrade_report.py +0 -0
  244. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_urls_autodiscover.py +0 -0
  245. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_user_data_admin.py +0 -0
  246. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_version_endpoint.py +0 -0
  247. {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_version_file.py +0 -0
  248. {arthexis-0.1.17 → 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.17
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.17
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",
@@ -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")),
@@ -2947,7 +2947,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2947
2947
  "toggle_selected_released",
2948
2948
  "toggle_selected_allowed",
2949
2949
  ]
2950
- readonly_fields = ("added_on", "last_seen_on")
2950
+ readonly_fields = ("added_on", "last_seen_on", "reversed_uid")
2951
2951
  form = RFIDForm
2952
2952
 
2953
2953
  def get_import_resource_kwargs(self, request, form=None, **kwargs):
@@ -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,
@@ -1906,7 +1914,16 @@ class RFID(Entity):
1906
1914
  if self.key_b and old["key_b"] != self.key_b.upper():
1907
1915
  self.key_b_verified = False
1908
1916
  if self.rfid:
1909
- 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)
1910
1927
  if self.key_a:
1911
1928
  self.key_a = self.key_a.upper()
1912
1929
  if self.key_b:
@@ -1933,6 +1950,19 @@ class RFID(Entity):
1933
1950
  return candidate
1934
1951
  return cls.BIG_ENDIAN
1935
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
+
1936
1966
  @classmethod
1937
1967
  def next_scan_label(
1938
1968
  cls, *, step: int | None = None, start: int | None = None
@@ -550,6 +550,15 @@ class RFIDValidationTests(TestCase):
550
550
  tag = RFID.objects.create(rfid="DEADBEEF10")
551
551
  self.assertEqual(tag.rfid, "DEADBEEF10")
552
552
 
553
+ def test_reversed_uid_updates_with_rfid(self):
554
+ tag = RFID.objects.create(rfid="A1B2C3D4")
555
+ self.assertEqual(tag.reversed_uid, "D4C3B2A1")
556
+
557
+ tag.rfid = "112233"
558
+ tag.save(update_fields=["rfid"])
559
+ tag.refresh_from_db()
560
+ self.assertEqual(tag.reversed_uid, "332211")
561
+
553
562
  def test_find_user_by_rfid(self):
554
563
  user = User.objects.create_user(username="finder", password="pwd")
555
564
  acc = EnergyAccount.objects.create(user=user, name="FINDER")
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
43
43
  PYPI_REQUEST_TIMEOUT = 10
44
44
 
45
45
  from . import changelog as changelog_utils
46
+ from . import temp_passwords
46
47
  from .models import OdooProfile, Product, EnergyAccount, PackageRelease, Todo
47
48
  from .models import RFID
48
49
 
@@ -336,6 +337,35 @@ def odoo_quote_report(request):
336
337
  return TemplateResponse(request, "admin/core/odoo_quote_report.html", context)
337
338
 
338
339
 
340
+ @staff_member_required
341
+ @require_GET
342
+ def request_temp_password(request):
343
+ """Generate a temporary password for the authenticated staff member."""
344
+
345
+ user = request.user
346
+ username = user.get_username()
347
+ password = temp_passwords.generate_password()
348
+ entry = temp_passwords.store_temp_password(
349
+ username,
350
+ password,
351
+ allow_change=True,
352
+ )
353
+ context = {
354
+ **admin_site.each_context(request),
355
+ "title": _("Temporary password"),
356
+ "username": username,
357
+ "password": password,
358
+ "expires_at": timezone.localtime(entry.expires_at),
359
+ "allow_change": entry.allow_change,
360
+ "return_url": reverse("admin:password_change"),
361
+ }
362
+ return TemplateResponse(
363
+ request,
364
+ "admin/core/request_temp_password.html",
365
+ context,
366
+ )
367
+
368
+
339
369
  @require_GET
340
370
  def version_info(request):
341
371
  """Return the running application version and Git revision."""
@@ -1855,26 +1885,34 @@ def release_progress(request, pk: int, action: str):
1855
1885
 
1856
1886
  pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
1857
1887
  pending_items = list(pending_qs)
1858
- if ack_todos_requested:
1859
- if pending_items:
1860
- failures = []
1861
- for todo in pending_items:
1862
- result = todo.check_on_done_condition()
1863
- if not result.passed:
1864
- failures.append((todo, result))
1865
- if failures:
1866
- ctx.pop("todos_ack", None)
1867
- for todo, result in failures:
1868
- messages.error(request, _format_condition_failure(todo, result))
1869
- else:
1870
- ctx["todos_ack"] = True
1888
+ if not pending_items:
1889
+ ctx["todos_ack"] = True
1890
+ ctx["todos_ack_auto"] = True
1891
+ elif ack_todos_requested:
1892
+ failures = []
1893
+ for todo in pending_items:
1894
+ result = todo.check_on_done_condition()
1895
+ if not result.passed:
1896
+ failures.append((todo, result))
1897
+ if failures:
1898
+ ctx["todos_ack"] = False
1899
+ ctx.pop("todos_ack_auto", None)
1900
+ for todo, result in failures:
1901
+ messages.error(request, _format_condition_failure(todo, result))
1871
1902
  else:
1872
1903
  ctx["todos_ack"] = True
1904
+ ctx.pop("todos_ack_auto", None)
1905
+ else:
1906
+ if ctx.pop("todos_ack_auto", None):
1907
+ ctx["todos_ack"] = False
1908
+ else:
1909
+ ctx.setdefault("todos_ack", False)
1873
1910
 
1874
1911
  if ctx.get("todos_ack"):
1875
1912
  ctx.pop("todos_block_logged", None)
1876
-
1877
- if not ctx.get("todos_ack"):
1913
+ ctx.pop("todos", None)
1914
+ ctx.pop("todos_required", None)
1915
+ else:
1878
1916
  ctx["todos"] = [
1879
1917
  {
1880
1918
  "id": todo.pk,
@@ -1885,9 +1923,6 @@ def release_progress(request, pk: int, action: str):
1885
1923
  for todo in pending_items
1886
1924
  ]
1887
1925
  ctx["todos_required"] = True
1888
- else:
1889
- ctx.pop("todos", None)
1890
- ctx.pop("todos_required", None)
1891
1926
 
1892
1927
  log_name = _release_log_name(release.package.name, release.version)
1893
1928
  if ctx.get("log") != log_name:
@@ -1897,6 +1932,8 @@ def release_progress(request, pk: int, action: str):
1897
1932
  "started": ctx.get("started", False),
1898
1933
  }
1899
1934
  step_count = 0
1935
+ if not pending_items:
1936
+ ctx["todos_ack"] = True
1900
1937
  log_path = log_dir / log_name
1901
1938
  ctx.setdefault("log", log_name)
1902
1939
  ctx.setdefault("paused", False)
@@ -5,6 +5,7 @@ from datetime import datetime
5
5
  import asyncio
6
6
  import inspect
7
7
  import json
8
+ import logging
8
9
  from urllib.parse import parse_qs
9
10
  from django.utils import timezone
10
11
  from core.models import EnergyAccount, Reference, RFID as CoreRFID
@@ -32,6 +33,9 @@ from .evcs_discovery import (
32
33
  FORWARDED_PAIR_RE = re.compile(r"for=(?:\"?)(?P<value>[^;,\"\s]+)(?:\"?)", re.IGNORECASE)
33
34
 
34
35
 
36
+ logger = logging.getLogger(__name__)
37
+
38
+
35
39
  # Query parameter keys that may contain the charge point serial. Keys are
36
40
  # matched case-insensitively and trimmed before use.
37
41
  SERIAL_QUERY_PARAM_NAMES = (
@@ -309,6 +313,19 @@ class CSMSConsumer(AsyncWebsocketConsumer):
309
313
 
310
314
  return await database_sync_to_async(_ensure)()
311
315
 
316
+ def _log_unlinked_rfid(self, rfid: str) -> None:
317
+ """Record a warning when an RFID is authorized without an account."""
318
+
319
+ message = (
320
+ f"Authorized RFID {rfid} on charger {self.charger_id} without linked energy account"
321
+ )
322
+ logger.warning(message)
323
+ store.add_log(
324
+ store.pending_key(self.charger_id),
325
+ message,
326
+ log_type="charger",
327
+ )
328
+
312
329
  async def _assign_connector(self, connector: int | str | None) -> None:
313
330
  """Ensure ``self.charger`` matches the provided connector id."""
314
331
  if connector in (None, "", "-"):
@@ -1395,13 +1412,25 @@ class CSMSConsumer(AsyncWebsocketConsumer):
1395
1412
  elif action == "Authorize":
1396
1413
  id_tag = payload.get("idTag")
1397
1414
  account = await self._get_account(id_tag)
1415
+ status = "Invalid"
1398
1416
  if self.charger.require_rfid:
1399
- status = (
1400
- "Accepted"
1401
- if account
1402
- and await database_sync_to_async(account.can_authorize)()
1403
- else "Invalid"
1404
- )
1417
+ tag = None
1418
+ tag_created = False
1419
+ if id_tag:
1420
+ tag, tag_created = await database_sync_to_async(
1421
+ CoreRFID.register_scan
1422
+ )(id_tag)
1423
+ if account:
1424
+ if await database_sync_to_async(account.can_authorize)():
1425
+ status = "Accepted"
1426
+ elif (
1427
+ id_tag
1428
+ and tag
1429
+ and not tag_created
1430
+ and tag.allowed
1431
+ ):
1432
+ status = "Accepted"
1433
+ self._log_unlinked_rfid(tag.rfid)
1405
1434
  else:
1406
1435
  await self._ensure_rfid_seen(id_tag)
1407
1436
  status = "Accepted"
@@ -1475,23 +1504,38 @@ class CSMSConsumer(AsyncWebsocketConsumer):
1475
1504
  reply_payload = {}
1476
1505
  elif action == "StartTransaction":
1477
1506
  id_tag = payload.get("idTag")
1478
- account = await self._get_account(id_tag)
1507
+ tag = None
1508
+ tag_created = False
1479
1509
  if id_tag:
1480
- if self.charger.require_rfid:
1481
- await database_sync_to_async(CoreRFID.register_scan)(
1482
- id_tag.upper()
1483
- )
1484
- else:
1485
- await self._ensure_rfid_seen(id_tag)
1510
+ tag, tag_created = await database_sync_to_async(
1511
+ CoreRFID.register_scan
1512
+ )(id_tag)
1513
+ account = await self._get_account(id_tag)
1514
+ if id_tag and not self.charger.require_rfid:
1515
+ seen_tag = await self._ensure_rfid_seen(id_tag)
1516
+ if seen_tag:
1517
+ tag = seen_tag
1486
1518
  await self._assign_connector(payload.get("connectorId"))
1519
+ authorized = True
1520
+ authorized_via_tag = False
1487
1521
  if self.charger.require_rfid:
1488
- authorized = (
1489
- account is not None
1490
- and await database_sync_to_async(account.can_authorize)()
1491
- )
1492
- else:
1493
- authorized = True
1522
+ if account is not None:
1523
+ authorized = await database_sync_to_async(
1524
+ account.can_authorize
1525
+ )()
1526
+ elif (
1527
+ id_tag
1528
+ and tag
1529
+ and not tag_created
1530
+ and getattr(tag, "allowed", False)
1531
+ ):
1532
+ authorized = True
1533
+ authorized_via_tag = True
1534
+ else:
1535
+ authorized = False
1494
1536
  if authorized:
1537
+ if authorized_via_tag and tag:
1538
+ self._log_unlinked_rfid(tag.rfid)
1495
1539
  start_timestamp = _parse_ocpp_timestamp(payload.get("timestamp"))
1496
1540
  received_start = timezone.now()
1497
1541
  tx_obj = await database_sync_to_async(Transaction.objects.create)(
@@ -519,6 +519,76 @@ class ValidateRfidValueTests(SimpleTestCase):
519
519
  mock_popen.assert_not_called()
520
520
  self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
521
521
 
522
+ @patch("ocpp.rfid.reader.timezone.now")
523
+ @patch("ocpp.rfid.reader.notify_async")
524
+ @patch("ocpp.rfid.reader.subprocess.Popen")
525
+ @patch("ocpp.rfid.reader.subprocess.run")
526
+ @patch("ocpp.rfid.reader.RFID.register_scan")
527
+ def test_external_command_strips_trailing_percent_tokens(
528
+ self, mock_register, mock_run, mock_popen, mock_notify, mock_now
529
+ ):
530
+ mock_now.return_value = timezone.now()
531
+ tag = MagicMock()
532
+ tag.pk = 3
533
+ tag.label_id = 3
534
+ tag.allowed = True
535
+ tag.external_command = "echo weird"
536
+ tag.color = "Y"
537
+ tag.released = False
538
+ tag.reference = None
539
+ tag.kind = RFID.CLASSIC
540
+ tag.endianness = RFID.BIG_ENDIAN
541
+ mock_register.return_value = (tag, False)
542
+ mock_run.return_value = types.SimpleNamespace(
543
+ returncode=0,
544
+ stdout="first %\nsecond 50%\r\nthird % %\n",
545
+ stderr="oops %\n",
546
+ )
547
+
548
+ result = validate_rfid_value("abc3")
549
+
550
+ output = result.get("command_output")
551
+ self.assertIsNotNone(output)
552
+ self.assertEqual(
553
+ output.get("stdout"), "first\nsecond 50%\r\nthird\n"
554
+ )
555
+ self.assertEqual(output.get("stderr"), "oops\n")
556
+ self.assertEqual(output.get("returncode"), 0)
557
+ self.assertEqual(output.get("error"), "")
558
+ mock_popen.assert_not_called()
559
+
560
+ @patch("ocpp.rfid.reader.timezone.now")
561
+ @patch("ocpp.rfid.reader.notify_async")
562
+ @patch("ocpp.rfid.reader.subprocess.Popen")
563
+ @patch("ocpp.rfid.reader.subprocess.run")
564
+ @patch("ocpp.rfid.reader.RFID.register_scan")
565
+ def test_external_command_error_strips_trailing_percent_tokens(
566
+ self, mock_register, mock_run, mock_popen, mock_notify, mock_now
567
+ ):
568
+ mock_now.return_value = timezone.now()
569
+ tag = MagicMock()
570
+ tag.pk = 4
571
+ tag.label_id = 4
572
+ tag.allowed = True
573
+ tag.external_command = "echo boom"
574
+ tag.color = "R"
575
+ tag.released = False
576
+ tag.reference = None
577
+ tag.kind = RFID.CLASSIC
578
+ tag.endianness = RFID.BIG_ENDIAN
579
+ mock_register.return_value = (tag, False)
580
+ mock_run.side_effect = RuntimeError("bad % %")
581
+
582
+ result = validate_rfid_value("abcd")
583
+
584
+ output = result.get("command_output")
585
+ self.assertIsInstance(output, dict)
586
+ self.assertEqual(output.get("stdout"), "")
587
+ self.assertEqual(output.get("stderr"), "")
588
+ self.assertEqual(output.get("error"), "bad")
589
+ self.assertFalse(result["allowed"])
590
+ mock_popen.assert_not_called()
591
+
522
592
  @patch("ocpp.rfid.reader.timezone.now")
523
593
  @patch("ocpp.rfid.reader.notify_async")
524
594
  @patch("ocpp.rfid.reader.subprocess.Popen")
@@ -2949,6 +2949,44 @@ class SimulatorAdminTests(TransactionTestCase):
2949
2949
 
2950
2950
  await communicator.disconnect()
2951
2951
 
2952
+ async def test_authorize_requires_rfid_accepts_allowed_tag_without_account(self):
2953
+ charger_id = "AUTHWARN"
2954
+ tag_value = "WARN01"
2955
+ await database_sync_to_async(Charger.objects.create)(
2956
+ charger_id=charger_id, require_rfid=True
2957
+ )
2958
+ await database_sync_to_async(RFID.objects.create)(rfid=tag_value, allowed=True)
2959
+
2960
+ pending_key = store.pending_key(charger_id)
2961
+ store.clear_log(pending_key, log_type="charger")
2962
+
2963
+ communicator = WebsocketCommunicator(application, f"/{charger_id}/")
2964
+ connected, _ = await communicator.connect()
2965
+ self.assertTrue(connected)
2966
+
2967
+ message_id = "auth-unlinked"
2968
+ await communicator.send_json_to(
2969
+ [2, message_id, "Authorize", {"idTag": tag_value}]
2970
+ )
2971
+ response = await communicator.receive_json_from()
2972
+ self.assertEqual(response[0], 3)
2973
+ self.assertEqual(response[1], message_id)
2974
+ self.assertEqual(response[2], {"idTagInfo": {"status": "Accepted"}})
2975
+
2976
+ log_entries = store.get_logs(pending_key, log_type="charger")
2977
+ self.assertTrue(
2978
+ any(
2979
+ "Authorized RFID" in entry
2980
+ and tag_value in entry
2981
+ and charger_id in entry
2982
+ for entry in log_entries
2983
+ ),
2984
+ log_entries,
2985
+ )
2986
+
2987
+ await communicator.disconnect()
2988
+ store.clear_log(pending_key, log_type="charger")
2989
+
2952
2990
  async def test_authorize_without_requirement_records_rfid(self):
2953
2991
  await database_sync_to_async(Charger.objects.create)(
2954
2992
  charger_id="AUTHOPT", require_rfid=False
@@ -3041,6 +3079,61 @@ class SimulatorAdminTests(TransactionTestCase):
3041
3079
  )
3042
3080
  self.assertEqual(tx.account_id, user.energy_account.id)
3043
3081
 
3082
+ async def test_start_transaction_allows_allowed_tag_without_account(self):
3083
+ charger_id = "STARTWARN"
3084
+ tag_value = "WARN02"
3085
+ await database_sync_to_async(Charger.objects.create)(
3086
+ charger_id=charger_id, require_rfid=True
3087
+ )
3088
+ await database_sync_to_async(RFID.objects.create)(rfid=tag_value, allowed=True)
3089
+
3090
+ pending_key = store.pending_key(charger_id)
3091
+ store.clear_log(pending_key, log_type="charger")
3092
+
3093
+ communicator = WebsocketCommunicator(application, f"/{charger_id}/")
3094
+ connected, _ = await communicator.connect()
3095
+ self.assertTrue(connected)
3096
+
3097
+ start_payload = {
3098
+ "meterStart": 5,
3099
+ "idTag": tag_value,
3100
+ "connectorId": 1,
3101
+ }
3102
+ await communicator.send_json_to([2, "start-1", "StartTransaction", start_payload])
3103
+ response = await communicator.receive_json_from()
3104
+ self.assertEqual(response[0], 3)
3105
+ self.assertEqual(response[2]["idTagInfo"]["status"], "Accepted")
3106
+ tx_id = response[2]["transactionId"]
3107
+
3108
+ tx = await database_sync_to_async(Transaction.objects.get)(
3109
+ pk=tx_id, charger__charger_id=charger_id
3110
+ )
3111
+ self.assertIsNone(tx.account_id)
3112
+
3113
+ log_entries = store.get_logs(pending_key, log_type="charger")
3114
+ self.assertTrue(
3115
+ any(
3116
+ "Authorized RFID" in entry
3117
+ and tag_value in entry
3118
+ and charger_id in entry
3119
+ for entry in log_entries
3120
+ ),
3121
+ log_entries,
3122
+ )
3123
+
3124
+ await communicator.send_json_to(
3125
+ [
3126
+ 2,
3127
+ "stop-1",
3128
+ "StopTransaction",
3129
+ {"transactionId": tx_id, "meterStop": 6},
3130
+ ]
3131
+ )
3132
+ await communicator.receive_json_from()
3133
+
3134
+ await communicator.disconnect()
3135
+ store.clear_log(pending_key, log_type="charger")
3136
+
3044
3137
  async def test_status_fields_updated(self):
3045
3138
  communicator = WebsocketCommunicator(application, "/STAT/")
3046
3139
  connected, _ = await communicator.connect()
@@ -1209,17 +1209,38 @@ def charger_log_page(request, cid, connector=None):
1209
1209
  charger_id=cid
1210
1210
  )
1211
1211
  target_id = cid
1212
- log = store.get_logs(target_id, log_type=log_type)
1212
+ limit_options = [
1213
+ {"value": "10", "label": "10"},
1214
+ {"value": "20", "label": "20"},
1215
+ {"value": "40", "label": "40"},
1216
+ {"value": "100", "label": "100"},
1217
+ {"value": "all", "label": gettext("All")},
1218
+ ]
1219
+ allowed_values = [item["value"] for item in limit_options]
1220
+ limit_choice = request.GET.get("limit", "20")
1221
+ if limit_choice not in allowed_values:
1222
+ limit_choice = "20"
1223
+
1224
+ log_entries = list(store.get_logs(target_id, log_type=log_type) or [])
1225
+ if limit_choice != "all":
1226
+ try:
1227
+ limit_value = int(limit_choice)
1228
+ except (TypeError, ValueError):
1229
+ limit_value = 20
1230
+ limit_choice = "20"
1231
+ log_entries = log_entries[-limit_value:]
1213
1232
  return render(
1214
1233
  request,
1215
1234
  "ocpp/charger_logs.html",
1216
1235
  {
1217
1236
  "charger": charger,
1218
- "log": log,
1237
+ "log": log_entries,
1219
1238
  "log_type": log_type,
1220
1239
  "connector_slug": connector_slug,
1221
1240
  "connector_links": connector_links,
1222
1241
  "status_url": status_url,
1242
+ "log_limit_options": limit_options,
1243
+ "log_limit_index": allowed_values.index(limit_choice),
1223
1244
  },
1224
1245
  )
1225
1246