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.
- {arthexis-0.1.17 → arthexis-0.1.18}/PKG-INFO +1 -1
- {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/PKG-INFO +1 -1
- {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/SOURCES.txt +2 -0
- arthexis-0.1.18/config/middleware.py +71 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/settings.py +1 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/urls.py +5 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/admin.py +1 -1
- {arthexis-0.1.17 → arthexis-0.1.18}/core/models.py +31 -1
- {arthexis-0.1.17 → arthexis-0.1.18}/core/tests.py +9 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/views.py +55 -18
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/consumers.py +63 -19
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/test_rfid.py +70 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/tests.py +93 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/views.py +23 -2
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/admin.py +87 -5
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/apps.py +3 -0
- arthexis-0.1.18/pages/site_config.py +137 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/tests.py +180 -1
- {arthexis-0.1.17 → arthexis-0.1.18}/pyproject.toml +1 -1
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_progress.py +13 -0
- arthexis-0.1.18/tests/test_render_nginx_sites.py +72 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_seed_data.py +61 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_temp_passwords.py +30 -1
- arthexis-0.1.17/config/middleware.py +0 -25
- {arthexis-0.1.17 → arthexis-0.1.18}/LICENSE +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/README.md +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/dependency_links.txt +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/requires.txt +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/arthexis.egg-info/top_level.txt +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/__init__.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/active_app.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/asgi.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/auth_app.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/celery.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/context_processors.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/horologia_app.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/loadenv.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/logging.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/offline.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/settings_helpers.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/config/wsgi.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/__init__.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/admin_history.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/admindocs.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/apps.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/auto_upgrade.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/backends.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/changelog.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/entity.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/environment.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/fields.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/form_fields.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/github_helper.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/github_issues.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/github_repos.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/lcd_screen.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/liveupdate.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/log_paths.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/mailer.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/middleware.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/notifications.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/public_wifi.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/reference_utils.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/release.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/rfid_import_export.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/sigil_builder.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/sigil_context.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/sigil_resolver.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/system.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/tasks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/temp_passwords.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/test_system_info.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/tests_liveupdate.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/urls.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/user_data.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/widgets.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/workgroup_urls.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/core/workgroup_views.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/__init__.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/apps.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/backends.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/dns.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/feature_checks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/lcd.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/models.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/reports.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/rfid_sync.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/signals.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/tasks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/tests.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/urls.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/utils.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/nodes/views.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/__init__.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/apps.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/evcs.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/evcs_discovery.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/models.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/reference_utils.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/routing.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/simulator.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/status_display.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/store.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/tasks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/test_export_import.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/transactions_io.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/ocpp/urls.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/__init__.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/checks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/context_processors.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/defaults.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/forms.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/middleware.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/models.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/module_defaults.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/tasks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/urls.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/utils.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/pages/views.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/setup.cfg +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_acronym_capitalization.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_client_report.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_doc_commands.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_doc_model_groups.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_history.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_index_actions.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_model_graph.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_object_history.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_profile_link.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_admin_system_stop.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_allowed_hosts_hostname.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_api_login_required.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_assistant_data_api.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_assistant_profile_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_assistant_profile_api.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_auto_upgrade_scheduler.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_awg_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_benchmark_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_birthday_greetings.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_build_pypi_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_celery_no_debug.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_changelog_builder.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_check_admin_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_check_migrations_script.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_check_pypi_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_clean_release_logs_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_client_report_generation.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_client_report_schedule.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_csrf_failure.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_csrf_origin_subnet.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_dist_cleanup.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_collector.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_inbox.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_inbox_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_inbox_search_action.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_outbox_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_profiles.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_email_transaction.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_env_refresh_clean.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_env_refresh_pip.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_env_refresh_unlink.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_experience_admin_group.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_fixture_presence.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_footer_no_references.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_footer_presence.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_footer_render.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_git_checks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_github_issue_reporting.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_install_script.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_invitation_login_view.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_language_switch.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_lcd_check_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_lcd_smbus2.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_localhost_admin_backend.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_log_paths.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_login_view_no_site.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_manage_debug_flag.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_manuals.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_asgi.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_auto_start.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_process.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_sigil_server.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_mcp_sigil_server_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_message_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_migrations.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_model_verbose_name_capitalization.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_network_setup_interactive.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_node_info_view.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_notifications_fallback.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_notify_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_ocpp_session_lock.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_product.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_profile.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_profile_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_odoo_quote_report.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_offline.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_package_admin_next_release.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_power_admin_group.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_profile_inline_deletion.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_pypi_check.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_pypi_token.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_readme_language.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_reference_qr_code.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_reference_transaction_uuid.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_register_site_apps_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_build.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_build_flow.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_checklist.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_logs.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_manager_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_packages.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_progress_pre_release_integration.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_push.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_release_tasks.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_request_invite.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_admin_print_labels.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_admin_reference_clear.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_admin_scan_csrf.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_always_on.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_backend.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_background_reader.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_client_report.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_rfid_watch_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_role_marker_filtering.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_send_invite_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_settings_helpers.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_shell_scripts.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_show_leads_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_sigil_builder.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_sigil_resolution.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_sites_utils.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_social_profile.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_staff_login_net_message.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_staff_required_decorator.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_switch_role_script.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_system_changelog_report.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_totp_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_totp_backend.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_uninstall_script.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_update_fixtures_command.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_upgrade_report.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_urls_autodiscover.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_user_data_admin.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_version_endpoint.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_version_file.py +0 -0
- {arthexis-0.1.17 → arthexis-0.1.18}/tests/test_vscode_manage.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
if
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
1507
|
+
tag = None
|
|
1508
|
+
tag_created = False
|
|
1479
1509
|
if id_tag:
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
|