arthexis 0.1.21__tar.gz → 0.1.23__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 (247) hide show
  1. {arthexis-0.1.21 → arthexis-0.1.23}/PKG-INFO +9 -8
  2. {arthexis-0.1.21 → arthexis-0.1.23}/README.md +1 -0
  3. {arthexis-0.1.21 → arthexis-0.1.23}/arthexis.egg-info/PKG-INFO +9 -8
  4. {arthexis-0.1.21 → arthexis-0.1.23}/arthexis.egg-info/SOURCES.txt +3 -1
  5. {arthexis-0.1.21 → arthexis-0.1.23}/arthexis.egg-info/requires.txt +7 -7
  6. {arthexis-0.1.21 → arthexis-0.1.23}/config/settings.py +4 -0
  7. {arthexis-0.1.21 → arthexis-0.1.23}/config/urls.py +5 -0
  8. {arthexis-0.1.21 → arthexis-0.1.23}/core/admin.py +224 -32
  9. arthexis-0.1.23/core/environment.py +60 -0
  10. {arthexis-0.1.21 → arthexis-0.1.23}/core/models.py +903 -65
  11. {arthexis-0.1.21 → arthexis-0.1.23}/core/release.py +0 -5
  12. {arthexis-0.1.21 → arthexis-0.1.23}/core/system.py +76 -0
  13. {arthexis-0.1.21 → arthexis-0.1.23}/core/tests.py +181 -9
  14. {arthexis-0.1.21 → arthexis-0.1.23}/core/user_data.py +42 -2
  15. {arthexis-0.1.21 → arthexis-0.1.23}/core/views.py +68 -27
  16. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/admin.py +211 -60
  17. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/apps.py +11 -0
  18. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/models.py +35 -7
  19. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/tests.py +288 -1
  20. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/views.py +101 -48
  21. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/admin.py +32 -2
  22. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/consumers.py +1 -0
  23. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/models.py +52 -3
  24. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/tasks.py +99 -1
  25. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/tests.py +350 -2
  26. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/views.py +300 -6
  27. {arthexis-0.1.21 → arthexis-0.1.23}/pages/admin.py +112 -15
  28. arthexis-0.1.23/pages/apps.py +45 -0
  29. {arthexis-0.1.21 → arthexis-0.1.23}/pages/forms.py +31 -8
  30. {arthexis-0.1.21 → arthexis-0.1.23}/pages/models.py +42 -2
  31. {arthexis-0.1.21 → arthexis-0.1.23}/pages/tests.py +386 -28
  32. {arthexis-0.1.21 → arthexis-0.1.23}/pages/urls.py +10 -0
  33. {arthexis-0.1.21 → arthexis-0.1.23}/pages/views.py +347 -18
  34. {arthexis-0.1.21 → arthexis-0.1.23}/pyproject.toml +2 -2
  35. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_client_report.py +44 -14
  36. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_system_stop.py +18 -3
  37. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_client_report_generation.py +32 -9
  38. arthexis-0.1.23/tests/test_csrf_origin_subnet.py +138 -0
  39. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_fixture_presence.py +18 -6
  40. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_network_setup_interactive.py +3 -7
  41. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_odoo_product.py +36 -0
  42. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_power_admin_group.py +1 -1
  43. arthexis-0.1.23/tests/test_readme_assets.py +87 -0
  44. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_progress.py +45 -0
  45. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_seed_data.py +10 -5
  46. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_shell_scripts.py +5 -0
  47. arthexis-0.1.23/tests/test_stop_script.py +44 -0
  48. arthexis-0.1.23/tests/test_transaction_meter_readings.py +113 -0
  49. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_uninstall_script.py +2 -2
  50. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_version_endpoint.py +15 -0
  51. arthexis-0.1.21/core/environment.py +0 -297
  52. arthexis-0.1.21/pages/apps.py +0 -13
  53. arthexis-0.1.21/tests/test_csrf_origin_subnet.py +0 -21
  54. arthexis-0.1.21/tests/test_environment_network_setup_form.py +0 -39
  55. {arthexis-0.1.21 → arthexis-0.1.23}/LICENSE +0 -0
  56. {arthexis-0.1.21 → arthexis-0.1.23}/arthexis.egg-info/dependency_links.txt +0 -0
  57. {arthexis-0.1.21 → arthexis-0.1.23}/arthexis.egg-info/top_level.txt +0 -0
  58. {arthexis-0.1.21 → arthexis-0.1.23}/config/__init__.py +0 -0
  59. {arthexis-0.1.21 → arthexis-0.1.23}/config/active_app.py +0 -0
  60. {arthexis-0.1.21 → arthexis-0.1.23}/config/asgi.py +0 -0
  61. {arthexis-0.1.21 → arthexis-0.1.23}/config/auth_app.py +0 -0
  62. {arthexis-0.1.21 → arthexis-0.1.23}/config/celery.py +0 -0
  63. {arthexis-0.1.21 → arthexis-0.1.23}/config/context_processors.py +0 -0
  64. {arthexis-0.1.21 → arthexis-0.1.23}/config/horologia_app.py +0 -0
  65. {arthexis-0.1.21 → arthexis-0.1.23}/config/loadenv.py +0 -0
  66. {arthexis-0.1.21 → arthexis-0.1.23}/config/logging.py +0 -0
  67. {arthexis-0.1.21 → arthexis-0.1.23}/config/middleware.py +0 -0
  68. {arthexis-0.1.21 → arthexis-0.1.23}/config/offline.py +0 -0
  69. {arthexis-0.1.21 → arthexis-0.1.23}/config/settings_helpers.py +0 -0
  70. {arthexis-0.1.21 → arthexis-0.1.23}/config/wsgi.py +0 -0
  71. {arthexis-0.1.21 → arthexis-0.1.23}/core/__init__.py +0 -0
  72. {arthexis-0.1.21 → arthexis-0.1.23}/core/admin_history.py +0 -0
  73. {arthexis-0.1.21 → arthexis-0.1.23}/core/admindocs.py +0 -0
  74. {arthexis-0.1.21 → arthexis-0.1.23}/core/apps.py +0 -0
  75. {arthexis-0.1.21 → arthexis-0.1.23}/core/auto_upgrade.py +0 -0
  76. {arthexis-0.1.21 → arthexis-0.1.23}/core/backends.py +0 -0
  77. {arthexis-0.1.21 → arthexis-0.1.23}/core/changelog.py +0 -0
  78. {arthexis-0.1.21 → arthexis-0.1.23}/core/entity.py +0 -0
  79. {arthexis-0.1.21 → arthexis-0.1.23}/core/fields.py +0 -0
  80. {arthexis-0.1.21 → arthexis-0.1.23}/core/form_fields.py +0 -0
  81. {arthexis-0.1.21 → arthexis-0.1.23}/core/github_helper.py +0 -0
  82. {arthexis-0.1.21 → arthexis-0.1.23}/core/github_issues.py +0 -0
  83. {arthexis-0.1.21 → arthexis-0.1.23}/core/github_repos.py +0 -0
  84. {arthexis-0.1.21 → arthexis-0.1.23}/core/lcd_screen.py +0 -0
  85. {arthexis-0.1.21 → arthexis-0.1.23}/core/liveupdate.py +0 -0
  86. {arthexis-0.1.21 → arthexis-0.1.23}/core/log_paths.py +0 -0
  87. {arthexis-0.1.21 → arthexis-0.1.23}/core/mailer.py +0 -0
  88. {arthexis-0.1.21 → arthexis-0.1.23}/core/middleware.py +0 -0
  89. {arthexis-0.1.21 → arthexis-0.1.23}/core/notifications.py +0 -0
  90. {arthexis-0.1.21 → arthexis-0.1.23}/core/public_wifi.py +0 -0
  91. {arthexis-0.1.21 → arthexis-0.1.23}/core/reference_utils.py +0 -0
  92. {arthexis-0.1.21 → arthexis-0.1.23}/core/rfid_import_export.py +0 -0
  93. {arthexis-0.1.21 → arthexis-0.1.23}/core/sigil_builder.py +0 -0
  94. {arthexis-0.1.21 → arthexis-0.1.23}/core/sigil_context.py +0 -0
  95. {arthexis-0.1.21 → arthexis-0.1.23}/core/sigil_resolver.py +0 -0
  96. {arthexis-0.1.21 → arthexis-0.1.23}/core/tasks.py +0 -0
  97. {arthexis-0.1.21 → arthexis-0.1.23}/core/temp_passwords.py +0 -0
  98. {arthexis-0.1.21 → arthexis-0.1.23}/core/test_system_info.py +0 -0
  99. {arthexis-0.1.21 → arthexis-0.1.23}/core/tests_liveupdate.py +0 -0
  100. {arthexis-0.1.21 → arthexis-0.1.23}/core/urls.py +0 -0
  101. {arthexis-0.1.21 → arthexis-0.1.23}/core/widgets.py +0 -0
  102. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/__init__.py +0 -0
  103. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/backends.py +0 -0
  104. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/dns.py +0 -0
  105. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/feature_checks.py +0 -0
  106. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/lcd.py +0 -0
  107. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/reports.py +0 -0
  108. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/rfid_sync.py +0 -0
  109. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/signals.py +0 -0
  110. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/tasks.py +0 -0
  111. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/urls.py +0 -0
  112. {arthexis-0.1.21 → arthexis-0.1.23}/nodes/utils.py +0 -0
  113. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/__init__.py +0 -0
  114. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/apps.py +0 -0
  115. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/evcs.py +0 -0
  116. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/evcs_discovery.py +0 -0
  117. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/reference_utils.py +0 -0
  118. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/routing.py +0 -0
  119. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/simulator.py +0 -0
  120. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/status_display.py +0 -0
  121. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/store.py +0 -0
  122. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/test_export_import.py +0 -0
  123. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/test_rfid.py +0 -0
  124. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/transactions_io.py +0 -0
  125. {arthexis-0.1.21 → arthexis-0.1.23}/ocpp/urls.py +0 -0
  126. {arthexis-0.1.21 → arthexis-0.1.23}/pages/__init__.py +0 -0
  127. {arthexis-0.1.21 → arthexis-0.1.23}/pages/checks.py +0 -0
  128. {arthexis-0.1.21 → arthexis-0.1.23}/pages/context_processors.py +0 -0
  129. {arthexis-0.1.21 → arthexis-0.1.23}/pages/defaults.py +0 -0
  130. {arthexis-0.1.21 → arthexis-0.1.23}/pages/middleware.py +0 -0
  131. {arthexis-0.1.21 → arthexis-0.1.23}/pages/module_defaults.py +0 -0
  132. {arthexis-0.1.21 → arthexis-0.1.23}/pages/site_config.py +0 -0
  133. {arthexis-0.1.21 → arthexis-0.1.23}/pages/tasks.py +0 -0
  134. {arthexis-0.1.21 → arthexis-0.1.23}/pages/utils.py +0 -0
  135. {arthexis-0.1.21 → arthexis-0.1.23}/setup.cfg +0 -0
  136. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_acronym_capitalization.py +0 -0
  137. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_doc_commands.py +0 -0
  138. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_doc_model_groups.py +0 -0
  139. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_history.py +0 -0
  140. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_index_actions.py +0 -0
  141. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_model_graph.py +0 -0
  142. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_object_history.py +0 -0
  143. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_admin_profile_link.py +0 -0
  144. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_allowed_hosts_hostname.py +0 -0
  145. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_api_login_required.py +0 -0
  146. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_auto_upgrade_scheduler.py +0 -0
  147. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_awg_admin.py +0 -0
  148. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_benchmark_command.py +0 -0
  149. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_build_pypi_command.py +0 -0
  150. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_celery_no_debug.py +0 -0
  151. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_changelog_builder.py +0 -0
  152. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_check_admin_command.py +0 -0
  153. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_check_migrations_script.py +0 -0
  154. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_check_pypi_command.py +0 -0
  155. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_clean_release_logs_command.py +0 -0
  156. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_client_report_form.py +0 -0
  157. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_client_report_schedule.py +0 -0
  158. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_csrf_failure.py +0 -0
  159. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_dist_cleanup.py +0 -0
  160. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_email_collector.py +0 -0
  161. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_email_inbox.py +0 -0
  162. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_email_inbox_admin.py +0 -0
  163. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_email_inbox_search_action.py +0 -0
  164. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_email_outbox_admin.py +0 -0
  165. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_email_profiles.py +0 -0
  166. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_email_transaction.py +0 -0
  167. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_env_refresh_clean.py +0 -0
  168. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_env_refresh_pip.py +0 -0
  169. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_env_refresh_unlink.py +0 -0
  170. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_experience_admin_group.py +0 -0
  171. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_footer_no_references.py +0 -0
  172. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_footer_presence.py +0 -0
  173. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_footer_render.py +0 -0
  174. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_git_checks.py +0 -0
  175. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_github_issue_reporting.py +0 -0
  176. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_install_script.py +0 -0
  177. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_invitation_login_view.py +0 -0
  178. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_language_switch.py +0 -0
  179. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_lcd_check_command.py +0 -0
  180. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_lcd_smbus2.py +0 -0
  181. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_localhost_admin_backend.py +0 -0
  182. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_log_paths.py +0 -0
  183. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_login_view_no_site.py +0 -0
  184. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_manage_debug_flag.py +0 -0
  185. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_manuals.py +0 -0
  186. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_message_command.py +0 -0
  187. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_migrations.py +0 -0
  188. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_model_verbose_name_capitalization.py +0 -0
  189. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_node_info_view.py +0 -0
  190. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_notifications_fallback.py +0 -0
  191. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_notify_command.py +0 -0
  192. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_ocpp_session_lock.py +0 -0
  193. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_odoo_profile.py +0 -0
  194. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_odoo_profile_admin.py +0 -0
  195. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_odoo_quote_report.py +0 -0
  196. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_offline.py +0 -0
  197. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_package_admin_next_release.py +0 -0
  198. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_profile_inline_deletion.py +0 -0
  199. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_pypi_check.py +0 -0
  200. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_pypi_token.py +0 -0
  201. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_readme_editor.py +0 -0
  202. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_readme_language.py +0 -0
  203. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_reference_qr_code.py +0 -0
  204. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_reference_transaction_uuid.py +0 -0
  205. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_register_site_apps_command.py +0 -0
  206. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_build.py +0 -0
  207. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_build_flow.py +0 -0
  208. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_checklist.py +0 -0
  209. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_logs.py +0 -0
  210. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_manager_admin.py +0 -0
  211. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_packages.py +0 -0
  212. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_progress_pre_release_integration.py +0 -0
  213. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_push.py +0 -0
  214. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_release_tasks.py +0 -0
  215. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_render_nginx_sites.py +0 -0
  216. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_request_invite.py +0 -0
  217. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_admin_print_labels.py +0 -0
  218. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_admin_reference_clear.py +0 -0
  219. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_admin_scan_csrf.py +0 -0
  220. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_always_on.py +0 -0
  221. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_backend.py +0 -0
  222. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_background_reader.py +0 -0
  223. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_client_report.py +0 -0
  224. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_rfid_watch_command.py +0 -0
  225. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_role_marker_filtering.py +0 -0
  226. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_send_invite_command.py +0 -0
  227. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_settings_helpers.py +0 -0
  228. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_settings_mcp_port.py +0 -0
  229. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_show_leads_command.py +0 -0
  230. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_sigil_builder.py +0 -0
  231. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_sigil_resolution.py +0 -0
  232. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_sites_utils.py +0 -0
  233. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_social_profile.py +0 -0
  234. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_staff_login_net_message.py +0 -0
  235. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_staff_required_decorator.py +0 -0
  236. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_suite_gateway.py +0 -0
  237. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_switch_role_script.py +0 -0
  238. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_system_changelog_report.py +0 -0
  239. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_temp_passwords.py +0 -0
  240. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_totp_admin.py +0 -0
  241. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_totp_backend.py +0 -0
  242. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_update_fixtures_command.py +0 -0
  243. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_upgrade_report.py +0 -0
  244. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_urls_autodiscover.py +0 -0
  245. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_user_data_admin.py +0 -0
  246. {arthexis-0.1.21 → arthexis-0.1.23}/tests/test_version_file.py +0 -0
  247. {arthexis-0.1.21 → arthexis-0.1.23}/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.21
3
+ Version: 0.1.23
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
@@ -26,14 +26,14 @@ Requires-Dist: celery==5.5.3
26
26
  Requires-Dist: certifi==2025.7.14
27
27
  Requires-Dist: cffi==2.0.0
28
28
  Requires-Dist: channels==4.1.0
29
- Requires-Dist: charset-normalizer==3.4.2
29
+ Requires-Dist: charset-normalizer==3.4.4
30
30
  Requires-Dist: click==8.2.1
31
31
  Requires-Dist: click-didyoumean==0.3.1
32
32
  Requires-Dist: click-plugins==1.1.1.2
33
33
  Requires-Dist: click-repl==0.3.0
34
34
  Requires-Dist: colorama==0.4.6
35
35
  Requires-Dist: constantly==23.10.4
36
- Requires-Dist: cron-descriptor==1.4.5
36
+ Requires-Dist: cron-descriptor==2.0.6
37
37
  Requires-Dist: cryptography==45.0.5
38
38
  Requires-Dist: daphne==4.2.1
39
39
  Requires-Dist: diff-match-patch==20241021
@@ -56,15 +56,15 @@ Requires-Dist: hyperlink==21.0.0
56
56
  Requires-Dist: idna==3.11
57
57
  Requires-Dist: incremental==24.7.2
58
58
  Requires-Dist: kombu==5.5.4
59
- Requires-Dist: libipld==3.1.1
60
- Requires-Dist: Markdown==3.8.2
59
+ Requires-Dist: libipld==3.2.0
60
+ Requires-Dist: Markdown==3.9
61
61
  Requires-Dist: mdx_truly_sane_lists==1.3
62
62
  Requires-Dist: mfrc522==0.0.7; sys_platform == "linux"
63
63
  Requires-Dist: outcome==1.3.0.post0
64
64
  Requires-Dist: packaging==25.0
65
65
  Requires-Dist: pillow==11.3.0
66
66
  Requires-Dist: prompt_toolkit==3.0.51
67
- Requires-Dist: psutil==7.1.1
67
+ Requires-Dist: psutil==7.1.2
68
68
  Requires-Dist: psycopg==3.2.9
69
69
  Requires-Dist: psycopg-binary==3.2.12
70
70
  Requires-Dist: pyasn1==0.6.1
@@ -79,7 +79,7 @@ Requires-Dist: python-crontab==3.3.0
79
79
  Requires-Dist: python-dateutil==2.9.0.post0
80
80
  Requires-Dist: python-dotenv==1.1.1
81
81
  Requires-Dist: qrcode==8.2
82
- Requires-Dist: redis==5.0.8
82
+ Requires-Dist: redis==7.0.1
83
83
  Requires-Dist: reportlab==4.2.2
84
84
  Requires-Dist: requests==2.32.5
85
85
  Requires-Dist: selenium==4.34.2
@@ -103,7 +103,7 @@ Requires-Dist: typing_extensions==4.14.1
103
103
  Requires-Dist: tzdata==2025.2
104
104
  Requires-Dist: urllib3==2.5.0
105
105
  Requires-Dist: vine==5.1.0
106
- Requires-Dist: wcwidth==0.2.13
106
+ Requires-Dist: wcwidth==0.2.14
107
107
  Requires-Dist: webencodings==0.5.1
108
108
  Requires-Dist: websocket-client==1.8.0
109
109
  Requires-Dist: websockets==13.1
@@ -203,6 +203,7 @@ Terminal nodes can start directly with the scripts below without installing; Con
203
203
  - `--constellation` – enables the multi-user orchestration stack.
204
204
  - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
205
205
  - Upgrade with [`./upgrade.sh`](upgrade.sh).
206
+ - Consult the [Install & Lifecycle Scripts Manual](docs/development/install-lifecycle-scripts-manual.md) for complete flag descriptions and operational notes.
206
207
 
207
208
  - **Windows:**
208
209
  - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
@@ -88,6 +88,7 @@ Terminal nodes can start directly with the scripts below without installing; Con
88
88
  - `--constellation` – enables the multi-user orchestration stack.
89
89
  - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
90
90
  - Upgrade with [`./upgrade.sh`](upgrade.sh).
91
+ - Consult the [Install & Lifecycle Scripts Manual](docs/development/install-lifecycle-scripts-manual.md) for complete flag descriptions and operational notes.
91
92
 
92
93
  - **Windows:**
93
94
  - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.21
3
+ Version: 0.1.23
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
@@ -26,14 +26,14 @@ Requires-Dist: celery==5.5.3
26
26
  Requires-Dist: certifi==2025.7.14
27
27
  Requires-Dist: cffi==2.0.0
28
28
  Requires-Dist: channels==4.1.0
29
- Requires-Dist: charset-normalizer==3.4.2
29
+ Requires-Dist: charset-normalizer==3.4.4
30
30
  Requires-Dist: click==8.2.1
31
31
  Requires-Dist: click-didyoumean==0.3.1
32
32
  Requires-Dist: click-plugins==1.1.1.2
33
33
  Requires-Dist: click-repl==0.3.0
34
34
  Requires-Dist: colorama==0.4.6
35
35
  Requires-Dist: constantly==23.10.4
36
- Requires-Dist: cron-descriptor==1.4.5
36
+ Requires-Dist: cron-descriptor==2.0.6
37
37
  Requires-Dist: cryptography==45.0.5
38
38
  Requires-Dist: daphne==4.2.1
39
39
  Requires-Dist: diff-match-patch==20241021
@@ -56,15 +56,15 @@ Requires-Dist: hyperlink==21.0.0
56
56
  Requires-Dist: idna==3.11
57
57
  Requires-Dist: incremental==24.7.2
58
58
  Requires-Dist: kombu==5.5.4
59
- Requires-Dist: libipld==3.1.1
60
- Requires-Dist: Markdown==3.8.2
59
+ Requires-Dist: libipld==3.2.0
60
+ Requires-Dist: Markdown==3.9
61
61
  Requires-Dist: mdx_truly_sane_lists==1.3
62
62
  Requires-Dist: mfrc522==0.0.7; sys_platform == "linux"
63
63
  Requires-Dist: outcome==1.3.0.post0
64
64
  Requires-Dist: packaging==25.0
65
65
  Requires-Dist: pillow==11.3.0
66
66
  Requires-Dist: prompt_toolkit==3.0.51
67
- Requires-Dist: psutil==7.1.1
67
+ Requires-Dist: psutil==7.1.2
68
68
  Requires-Dist: psycopg==3.2.9
69
69
  Requires-Dist: psycopg-binary==3.2.12
70
70
  Requires-Dist: pyasn1==0.6.1
@@ -79,7 +79,7 @@ Requires-Dist: python-crontab==3.3.0
79
79
  Requires-Dist: python-dateutil==2.9.0.post0
80
80
  Requires-Dist: python-dotenv==1.1.1
81
81
  Requires-Dist: qrcode==8.2
82
- Requires-Dist: redis==5.0.8
82
+ Requires-Dist: redis==7.0.1
83
83
  Requires-Dist: reportlab==4.2.2
84
84
  Requires-Dist: requests==2.32.5
85
85
  Requires-Dist: selenium==4.34.2
@@ -103,7 +103,7 @@ Requires-Dist: typing_extensions==4.14.1
103
103
  Requires-Dist: tzdata==2025.2
104
104
  Requires-Dist: urllib3==2.5.0
105
105
  Requires-Dist: vine==5.1.0
106
- Requires-Dist: wcwidth==0.2.13
106
+ Requires-Dist: wcwidth==0.2.14
107
107
  Requires-Dist: webencodings==0.5.1
108
108
  Requires-Dist: websocket-client==1.8.0
109
109
  Requires-Dist: websockets==13.1
@@ -203,6 +203,7 @@ Terminal nodes can start directly with the scripts below without installing; Con
203
203
  - `--constellation` – enables the multi-user orchestration stack.
204
204
  - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
205
205
  - Upgrade with [`./upgrade.sh`](upgrade.sh).
206
+ - Consult the [Install & Lifecycle Scripts Manual](docs/development/install-lifecycle-scripts-manual.md) for complete flag descriptions and operational notes.
206
207
 
207
208
  - **Windows:**
208
209
  - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
@@ -149,7 +149,6 @@ tests/test_email_transaction.py
149
149
  tests/test_env_refresh_clean.py
150
150
  tests/test_env_refresh_pip.py
151
151
  tests/test_env_refresh_unlink.py
152
- tests/test_environment_network_setup_form.py
153
152
  tests/test_experience_admin_group.py
154
153
  tests/test_fixture_presence.py
155
154
  tests/test_footer_no_references.py
@@ -185,6 +184,7 @@ tests/test_power_admin_group.py
185
184
  tests/test_profile_inline_deletion.py
186
185
  tests/test_pypi_check.py
187
186
  tests/test_pypi_token.py
187
+ tests/test_readme_assets.py
188
188
  tests/test_readme_editor.py
189
189
  tests/test_readme_language.py
190
190
  tests/test_reference_qr_code.py
@@ -223,12 +223,14 @@ tests/test_sites_utils.py
223
223
  tests/test_social_profile.py
224
224
  tests/test_staff_login_net_message.py
225
225
  tests/test_staff_required_decorator.py
226
+ tests/test_stop_script.py
226
227
  tests/test_suite_gateway.py
227
228
  tests/test_switch_role_script.py
228
229
  tests/test_system_changelog_report.py
229
230
  tests/test_temp_passwords.py
230
231
  tests/test_totp_admin.py
231
232
  tests/test_totp_backend.py
233
+ tests/test_transaction_meter_readings.py
232
234
  tests/test_uninstall_script.py
233
235
  tests/test_update_fixtures_command.py
234
236
  tests/test_upgrade_report.py
@@ -13,14 +13,14 @@ celery==5.5.3
13
13
  certifi==2025.7.14
14
14
  cffi==2.0.0
15
15
  channels==4.1.0
16
- charset-normalizer==3.4.2
16
+ charset-normalizer==3.4.4
17
17
  click==8.2.1
18
18
  click-didyoumean==0.3.1
19
19
  click-plugins==1.1.1.2
20
20
  click-repl==0.3.0
21
21
  colorama==0.4.6
22
22
  constantly==23.10.4
23
- cron-descriptor==1.4.5
23
+ cron-descriptor==2.0.6
24
24
  cryptography==45.0.5
25
25
  daphne==4.2.1
26
26
  diff-match-patch==20241021
@@ -42,14 +42,14 @@ hyperlink==21.0.0
42
42
  idna==3.11
43
43
  incremental==24.7.2
44
44
  kombu==5.5.4
45
- libipld==3.1.1
46
- Markdown==3.8.2
45
+ libipld==3.2.0
46
+ Markdown==3.9
47
47
  mdx_truly_sane_lists==1.3
48
48
  outcome==1.3.0.post0
49
49
  packaging==25.0
50
50
  pillow==11.3.0
51
51
  prompt_toolkit==3.0.51
52
- psutil==7.1.1
52
+ psutil==7.1.2
53
53
  psycopg==3.2.9
54
54
  psycopg-binary==3.2.12
55
55
  pyasn1==0.6.1
@@ -64,7 +64,7 @@ python-crontab==3.3.0
64
64
  python-dateutil==2.9.0.post0
65
65
  python-dotenv==1.1.1
66
66
  qrcode==8.2
67
- redis==5.0.8
67
+ redis==7.0.1
68
68
  reportlab==4.2.2
69
69
  requests==2.32.5
70
70
  selenium==4.34.2
@@ -88,7 +88,7 @@ typing_extensions==4.14.1
88
88
  tzdata==2025.2
89
89
  urllib3==2.5.0
90
90
  vine==5.1.0
91
- wcwidth==0.2.13
91
+ wcwidth==0.2.14
92
92
  webencodings==0.5.1
93
93
  websocket-client==1.8.0
94
94
  websockets==13.1
@@ -662,4 +662,8 @@ CELERY_BEAT_SCHEDULE = {
662
662
  "task": "core.tasks.heartbeat",
663
663
  "schedule": crontab(minute="*/5"),
664
664
  },
665
+ "ocpp_configuration_check": {
666
+ "task": "ocpp.tasks.schedule_daily_charge_point_configuration_checks",
667
+ "schedule": crontab(minute=0, hour=0),
668
+ },
665
669
  }
@@ -134,6 +134,11 @@ urlpatterns = [
134
134
  core_views.todo_done,
135
135
  name="todo-done",
136
136
  ),
137
+ path(
138
+ "admin/core/todos/<int:pk>/delete/",
139
+ core_views.todo_delete,
140
+ name="todo-delete",
141
+ ),
137
142
  path(
138
143
  "admin/core/todos/<int:pk>/snapshot/",
139
144
  core_views.todo_snapshot,
@@ -9,10 +9,13 @@ from django.urls import NoReverseMatch, path, reverse
9
9
  from urllib.parse import urlencode, urlparse
10
10
  from django.shortcuts import get_object_or_404, redirect, render
11
11
  from django.http import (
12
+ FileResponse,
13
+ Http404,
12
14
  HttpResponse,
13
15
  JsonResponse,
14
16
  HttpResponseBase,
15
17
  HttpResponseRedirect,
18
+ HttpResponseNotAllowed,
16
19
  )
17
20
  from django.template.response import TemplateResponse
18
21
  from django.conf import settings
@@ -83,6 +86,7 @@ from .models import (
83
86
  OdooProfile,
84
87
  OpenPayProfile,
85
88
  EmailInbox,
89
+ GoogleCalendarProfile,
86
90
  SocialProfile,
87
91
  EmailCollector,
88
92
  Package,
@@ -605,15 +609,18 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
605
609
  change_actions = ["create_repository_action", "prepare_next_release_action"]
606
610
 
607
611
  def _prepare(self, request, package):
612
+ if request.method not in {"POST", "GET"}:
613
+ return HttpResponseNotAllowed(["GET", "POST"])
608
614
  from pathlib import Path
609
615
  from packaging.version import Version
610
616
 
611
617
  ver_file = Path("VERSION")
612
- repo_version = (
613
- Version(ver_file.read_text().strip())
614
- if ver_file.exists()
615
- else Version("0.0.0")
616
- )
618
+ if ver_file.exists():
619
+ raw_version = ver_file.read_text().strip()
620
+ cleaned_version = raw_version.rstrip("+") or "0.0.0"
621
+ repo_version = Version(cleaned_version)
622
+ else:
623
+ repo_version = Version("0.0.0")
617
624
 
618
625
  pypi_latest = Version("0.0.0")
619
626
  try:
@@ -1005,6 +1012,41 @@ class OpenPayProfileAdminForm(forms.ModelForm):
1005
1012
  )
1006
1013
 
1007
1014
 
1015
+ class GoogleCalendarProfileAdminForm(forms.ModelForm):
1016
+ """Admin form for :class:`core.models.GoogleCalendarProfile`."""
1017
+
1018
+ api_key = forms.CharField(
1019
+ widget=forms.PasswordInput(render_value=True),
1020
+ required=False,
1021
+ help_text="Leave blank to keep the current key.",
1022
+ )
1023
+
1024
+ class Meta:
1025
+ model = GoogleCalendarProfile
1026
+ fields = "__all__"
1027
+
1028
+ def __init__(self, *args, **kwargs):
1029
+ super().__init__(*args, **kwargs)
1030
+ if self.instance.pk:
1031
+ self.fields["api_key"].initial = ""
1032
+ self.initial["api_key"] = ""
1033
+ else:
1034
+ self.fields["api_key"].required = True
1035
+
1036
+ def clean_api_key(self):
1037
+ key = self.cleaned_data.get("api_key")
1038
+ if not key and self.instance.pk:
1039
+ return keep_existing("api_key")
1040
+ return key
1041
+
1042
+ def _post_clean(self):
1043
+ super()._post_clean()
1044
+ _restore_sigil_values(
1045
+ self,
1046
+ ["calendar_id", "api_key", "display_name", "timezone"],
1047
+ )
1048
+
1049
+
1008
1050
  class MaskedPasswordFormMixin:
1009
1051
  """Mixin that hides stored passwords while allowing updates."""
1010
1052
 
@@ -1222,6 +1264,15 @@ class OpenPayProfileInlineForm(ProfileFormMixin, OpenPayProfileAdminForm):
1222
1264
  return cleaned
1223
1265
 
1224
1266
 
1267
+ class GoogleCalendarProfileInlineForm(
1268
+ ProfileFormMixin, GoogleCalendarProfileAdminForm
1269
+ ):
1270
+ profile_fields = GoogleCalendarProfile.profile_fields
1271
+
1272
+ class Meta(GoogleCalendarProfileAdminForm.Meta):
1273
+ exclude = ("user", "group")
1274
+
1275
+
1225
1276
  class EmailInboxInlineForm(ProfileFormMixin, EmailInboxAdminForm):
1226
1277
  profile_fields = EmailInbox.profile_fields
1227
1278
 
@@ -1346,6 +1397,16 @@ PROFILE_INLINE_CONFIG = {
1346
1397
  ),
1347
1398
  "readonly_fields": ("verified_on", "verification_reference"),
1348
1399
  },
1400
+ GoogleCalendarProfile: {
1401
+ "form": GoogleCalendarProfileInlineForm,
1402
+ "fields": (
1403
+ "display_name",
1404
+ "calendar_id",
1405
+ "api_key",
1406
+ "max_events",
1407
+ "timezone",
1408
+ ),
1409
+ },
1349
1410
  EmailInbox: {
1350
1411
  "form": EmailInboxInlineForm,
1351
1412
  "fields": (
@@ -1817,6 +1878,44 @@ class OpenPayProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModel
1817
1878
  verify_credentials_action.short_description = _("Test credentials")
1818
1879
 
1819
1880
 
1881
+ class GoogleCalendarProfileAdmin(
1882
+ ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
1883
+ ):
1884
+ form = GoogleCalendarProfileAdminForm
1885
+ list_display = ("owner", "calendar_identifier", "max_events")
1886
+ search_fields = (
1887
+ "display_name",
1888
+ "calendar_id",
1889
+ "user__username",
1890
+ "group__name",
1891
+ )
1892
+ changelist_actions = ["my_profile"]
1893
+ change_actions = ["my_profile_action"]
1894
+ fieldsets = (
1895
+ (_("Owner"), {"fields": ("user", "group")}),
1896
+ (
1897
+ _("Calendar"),
1898
+ {
1899
+ "fields": (
1900
+ "display_name",
1901
+ "calendar_id",
1902
+ "api_key",
1903
+ "max_events",
1904
+ "timezone",
1905
+ )
1906
+ },
1907
+ ),
1908
+ )
1909
+
1910
+ @admin.display(description=_("Owner"))
1911
+ def owner(self, obj):
1912
+ return obj.owner_display()
1913
+
1914
+ @admin.display(description=_("Calendar"))
1915
+ def calendar_identifier(self, obj):
1916
+ display = obj.get_display_name()
1917
+ return display or obj.resolved_calendar_id()
1918
+
1820
1919
  class EmailSearchForm(forms.Form):
1821
1920
  subject = forms.CharField(
1822
1921
  required=False, widget=forms.TextInput(attrs={"style": "width: 40em;"})
@@ -2416,8 +2515,30 @@ class ProductAdmin(EntityModelAdmin):
2416
2515
  ],
2417
2516
  limit=0,
2418
2517
  )
2419
- except Exception:
2518
+ except Exception as exc:
2519
+ logger.exception(
2520
+ "Failed to fetch Odoo products for user %s (profile_id=%s, host=%s, database=%s)",
2521
+ getattr(getattr(request, "user", None), "pk", None),
2522
+ getattr(profile, "pk", None),
2523
+ getattr(profile, "host", None),
2524
+ getattr(profile, "database", None),
2525
+ )
2420
2526
  context["error"] = _("Unable to fetch products from Odoo.")
2527
+ if getattr(request.user, "is_superuser", False):
2528
+ fault = getattr(exc, "faultString", "")
2529
+ message = str(exc)
2530
+ details = [
2531
+ f"Host: {getattr(profile, 'host', '')}",
2532
+ f"Database: {getattr(profile, 'database', '')}",
2533
+ f"User ID: {getattr(profile, 'odoo_uid', '')}",
2534
+ ]
2535
+ if fault and fault != message:
2536
+ details.append(f"Fault: {fault}")
2537
+ if message:
2538
+ details.append(f"Exception: {type(exc).__name__}: {message}")
2539
+ else:
2540
+ details.append(f"Exception type: {type(exc).__name__}")
2541
+ context["debug_error"] = "\n".join(details)
2421
2542
  return context, []
2422
2543
 
2423
2544
  context["has_credentials"] = True
@@ -2697,7 +2818,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2697
2818
  "user_data_flag",
2698
2819
  "color",
2699
2820
  "kind",
2700
- "endianness",
2821
+ "endianness_short",
2701
2822
  "released",
2702
2823
  "allowed",
2703
2824
  "last_seen_on",
@@ -2762,6 +2883,14 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2762
2883
  def user_data_flag(self, obj):
2763
2884
  return getattr(obj, "is_user_data", False)
2764
2885
 
2886
+ @admin.display(description=_("End"), ordering="endianness")
2887
+ def endianness_short(self, obj):
2888
+ labels = {
2889
+ RFID.BIG_ENDIAN: _("Big"),
2890
+ RFID.LITTLE_ENDIAN: _("Little"),
2891
+ }
2892
+ return labels.get(obj.endianness, obj.get_endianness_display())
2893
+
2765
2894
  def scan_rfids(self, request, queryset):
2766
2895
  return redirect("admin:core_rfid_scan")
2767
2896
 
@@ -3547,10 +3676,10 @@ class ClientReportAdmin(EntityModelAdmin):
3547
3676
  initial=ClientReportSchedule.PERIODICITY_NONE,
3548
3677
  help_text="Defines how often the report should be generated automatically.",
3549
3678
  )
3550
- disable_emails = forms.BooleanField(
3551
- label="Disable email delivery",
3679
+ enable_emails = forms.BooleanField(
3680
+ label="Enable email delivery",
3552
3681
  required=False,
3553
- help_text="Generate files without sending emails.",
3682
+ help_text="Send the report via email to the recipients listed above.",
3554
3683
  )
3555
3684
 
3556
3685
  def __init__(self, *args, request=None, **kwargs):
@@ -3614,6 +3743,11 @@ class ClientReportAdmin(EntityModelAdmin):
3614
3743
  self.admin_site.admin_view(self.generate_view),
3615
3744
  name="core_clientreport_generate",
3616
3745
  ),
3746
+ path(
3747
+ "download/<int:report_id>/",
3748
+ self.admin_site.admin_view(self.download_view),
3749
+ name="core_clientreport_download",
3750
+ ),
3617
3751
  ]
3618
3752
  return custom + urls
3619
3753
 
@@ -3621,16 +3755,20 @@ class ClientReportAdmin(EntityModelAdmin):
3621
3755
  form = self.ClientReportForm(request.POST or None, request=request)
3622
3756
  report = None
3623
3757
  schedule = None
3758
+ download_url = None
3624
3759
  if request.method == "POST" and form.is_valid():
3625
3760
  owner = form.cleaned_data.get("owner")
3626
3761
  if not owner and request.user.is_authenticated:
3627
3762
  owner = request.user
3763
+ enable_emails = form.cleaned_data.get("enable_emails", False)
3764
+ disable_emails = not enable_emails
3765
+ recipients = form.cleaned_data.get("destinations") if enable_emails else []
3628
3766
  report = ClientReport.generate(
3629
3767
  form.cleaned_data["start"],
3630
3768
  form.cleaned_data["end"],
3631
3769
  owner=owner,
3632
- recipients=form.cleaned_data.get("destinations"),
3633
- disable_emails=form.cleaned_data.get("disable_emails", False),
3770
+ recipients=recipients,
3771
+ disable_emails=disable_emails,
3634
3772
  )
3635
3773
  report.store_local_copy()
3636
3774
  recurrence = form.cleaned_data.get("recurrence")
@@ -3639,8 +3777,8 @@ class ClientReportAdmin(EntityModelAdmin):
3639
3777
  owner=owner,
3640
3778
  created_by=request.user if request.user.is_authenticated else None,
3641
3779
  periodicity=recurrence,
3642
- email_recipients=form.cleaned_data.get("destinations", []),
3643
- disable_emails=form.cleaned_data.get("disable_emails", False),
3780
+ email_recipients=recipients,
3781
+ disable_emails=disable_emails,
3644
3782
  )
3645
3783
  report.schedule = schedule
3646
3784
  report.save(update_fields=["schedule"])
@@ -3649,12 +3787,69 @@ class ClientReportAdmin(EntityModelAdmin):
3649
3787
  "Client report schedule created; future reports will be generated automatically.",
3650
3788
  messages.SUCCESS,
3651
3789
  )
3790
+ if disable_emails:
3791
+ self.message_user(
3792
+ request,
3793
+ "Consumer report generated. The download will begin automatically.",
3794
+ messages.SUCCESS,
3795
+ )
3796
+ redirect_url = f"{reverse('admin:core_clientreport_generate')}?download={report.pk}"
3797
+ return HttpResponseRedirect(redirect_url)
3798
+ download_param = request.GET.get("download")
3799
+ if download_param:
3800
+ try:
3801
+ download_report = ClientReport.objects.get(pk=download_param)
3802
+ except ClientReport.DoesNotExist:
3803
+ pass
3804
+ else:
3805
+ download_url = reverse(
3806
+ "admin:core_clientreport_download", args=[download_report.pk]
3807
+ )
3652
3808
  context = self.admin_site.each_context(request)
3653
- context.update({"form": form, "report": report, "schedule": schedule})
3809
+ context.update(
3810
+ {
3811
+ "form": form,
3812
+ "report": report,
3813
+ "schedule": schedule,
3814
+ "download_url": download_url,
3815
+ "previous_reports": self._build_report_history(request),
3816
+ }
3817
+ )
3654
3818
  return TemplateResponse(
3655
3819
  request, "admin/core/clientreport/generate.html", context
3656
3820
  )
3657
3821
 
3822
+ def download_view(self, request, report_id: int):
3823
+ report = get_object_or_404(ClientReport, pk=report_id)
3824
+ pdf_path = report.ensure_pdf()
3825
+ if not pdf_path.exists():
3826
+ raise Http404("Report file unavailable")
3827
+ filename = f"consumer-report-{report.start_date}-{report.end_date}.pdf"
3828
+ response = FileResponse(pdf_path.open("rb"), content_type="application/pdf")
3829
+ response["Content-Disposition"] = f'attachment; filename="{filename}"'
3830
+ return response
3831
+
3832
+ def _build_report_history(self, request):
3833
+ queryset = ClientReport.objects.order_by("-created_on")[:20]
3834
+ history = []
3835
+ for item in queryset:
3836
+ totals = item.rows_for_display.get("totals", {})
3837
+ history.append(
3838
+ {
3839
+ "instance": item,
3840
+ "download_url": reverse(
3841
+ "admin:core_clientreport_download", args=[item.pk]
3842
+ ),
3843
+ "email_enabled": not item.disable_emails,
3844
+ "recipients": item.recipients or [],
3845
+ "totals": {
3846
+ "total_kw": totals.get("total_kw", 0.0),
3847
+ "total_kw_period": totals.get("total_kw_period", 0.0),
3848
+ },
3849
+ }
3850
+ )
3851
+ return history
3852
+
3658
3853
 
3659
3854
  @admin.register(PackageRelease)
3660
3855
  class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
@@ -3715,9 +3910,9 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
3715
3910
  self.message_user(request, str(exc), messages.ERROR)
3716
3911
  return
3717
3912
  releases = resp.json().get("releases", {})
3718
- created = 0
3719
3913
  updated = 0
3720
3914
  restored = 0
3915
+ missing: list[str] = []
3721
3916
 
3722
3917
  for version, files in releases.items():
3723
3918
  release_on = self._release_on_from_files(files)
@@ -3742,23 +3937,11 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
3742
3937
  if update_fields:
3743
3938
  release.save(update_fields=update_fields)
3744
3939
  continue
3745
- PackageRelease.objects.create(
3746
- package=package,
3747
- release_manager=package.release_manager,
3748
- version=version,
3749
- revision="",
3750
- pypi_url=f"https://pypi.org/project/{package.name}/{version}/",
3751
- release_on=release_on,
3752
- )
3753
- created += 1
3940
+ missing.append(version)
3754
3941
 
3755
- if created or updated or restored:
3942
+ if updated or restored:
3756
3943
  PackageRelease.dump_fixture()
3757
3944
  message_parts = []
3758
- if created:
3759
- message_parts.append(
3760
- f"Created {created} release{'s' if created != 1 else ''} from PyPI"
3761
- )
3762
3945
  if updated:
3763
3946
  message_parts.append(
3764
3947
  f"Updated release date for {updated} release"
@@ -3769,8 +3952,17 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
3769
3952
  f"Restored {restored} release{'s' if restored != 1 else ''}"
3770
3953
  )
3771
3954
  self.message_user(request, "; ".join(message_parts), messages.SUCCESS)
3772
- else:
3773
- self.message_user(request, "No new releases found", messages.INFO)
3955
+ elif not missing:
3956
+ self.message_user(request, "No matching releases found", messages.INFO)
3957
+
3958
+ if missing:
3959
+ versions = ", ".join(sorted(missing))
3960
+ count = len(missing)
3961
+ message = (
3962
+ "Manual creation required for "
3963
+ f"{count} release{'s' if count != 1 else ''}: {versions}"
3964
+ )
3965
+ self.message_user(request, message, messages.WARNING)
3774
3966
 
3775
3967
  refresh_from_pypi.label = "Refresh from PyPI"
3776
3968
  refresh_from_pypi.short_description = "Refresh from PyPI"