update: fixes enum ticket handling
This commit is contained in:
parent
f13bc8d61e
commit
a3cfc06630
4 changed files with 116 additions and 49 deletions
74
gui/v2/actions/operation_result_dispatch.py
Executable file
74
gui/v2/actions/operation_result_dispatch.py
Executable file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import inspect
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
|
from core.models.Configuration import ConnectionChoice
|
||||||
|
from core.models.Result import ResultError
|
||||||
|
from core.services.prepare_tickets import ticket_tracker
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ENUM_MAP = {
|
||||||
|
ResultError.SUBSCRIPTION: (
|
||||||
|
ticket_tracker.wipe_one_ticket_sub,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
ResultError.CONNECTION: (
|
||||||
|
ConfigurationController.set_connection_enum,
|
||||||
|
{'connection_enum': ConnectionChoice.SYSTEM},
|
||||||
|
),
|
||||||
|
ResultError.INVALID_API_REPLY: None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_function(enum_map: dict[Enum, Any], enum_key: Enum) -> bool:
|
||||||
|
action = enum_map.get(enum_key)
|
||||||
|
if action is None:
|
||||||
|
return False
|
||||||
|
function, _ = _normalize_action(action)
|
||||||
|
return callable(function)
|
||||||
|
|
||||||
|
|
||||||
|
def error_dispatch(enum_map: dict[Enum, Any], enum_key: Enum, **kwargs: Any) -> Any:
|
||||||
|
if enum_key not in enum_map:
|
||||||
|
return None
|
||||||
|
action = enum_map[enum_key]
|
||||||
|
if action is None:
|
||||||
|
return None
|
||||||
|
function, default_kwargs = _normalize_action(action)
|
||||||
|
call_kwargs = dict(default_kwargs)
|
||||||
|
call_kwargs.update(kwargs)
|
||||||
|
return function(**_filter_kwargs(function, call_kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def has_result_action(result, enum_map: dict[Enum, Any] | None = None) -> bool:
|
||||||
|
return has_function(enum_map or DEFAULT_ENUM_MAP, result.error_type)
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_result_action(result, enum_map: dict[Enum, Any] | None = None, **kwargs: Any) -> Any:
|
||||||
|
return error_dispatch(enum_map or DEFAULT_ENUM_MAP, result.error_type, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_action(action: Any) -> tuple[Any, dict[str, Any]]:
|
||||||
|
if isinstance(action, tuple):
|
||||||
|
function = action[0]
|
||||||
|
if len(action) > 1 and isinstance(action[1], dict):
|
||||||
|
return function, action[1]
|
||||||
|
return function, {}
|
||||||
|
return action, {}
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_kwargs(function: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
signature = inspect.signature(function)
|
||||||
|
parameters = signature.parameters
|
||||||
|
if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters.values()):
|
||||||
|
return kwargs
|
||||||
|
accepted = {
|
||||||
|
name
|
||||||
|
for name, param in parameters.items()
|
||||||
|
if param.kind in (
|
||||||
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||||
|
inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {key: value for key, value in kwargs.items() if key in accepted}
|
||||||
|
|
@ -4,3 +4,15 @@ from core.models.Result import Result, ResultError
|
||||||
def result_from_exception(exception: Exception, error_type: ResultError = ResultError.UNKNOWN) -> Result:
|
def result_from_exception(exception: Exception, error_type: ResultError = ResultError.UNKNOWN) -> Result:
|
||||||
message = str(exception) or type(exception).__name__
|
message = str(exception) or type(exception).__name__
|
||||||
return Result(valid=False, error_type=error_type, message=message)
|
return Result(valid=False, error_type=error_type, message=message)
|
||||||
|
|
||||||
|
|
||||||
|
def result_from_payload(payload) -> Result:
|
||||||
|
if isinstance(payload, Result):
|
||||||
|
return payload
|
||||||
|
if isinstance(payload, Exception):
|
||||||
|
return result_from_exception(payload)
|
||||||
|
return Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.UNKNOWN,
|
||||||
|
message=str(payload) or 'The operation could not be completed.',
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,13 @@ from core.controllers.tickets.UseTicketController import (
|
||||||
do_we_use_a_random_ticket,
|
do_we_use_a_random_ticket,
|
||||||
get_unused_tickets,
|
get_unused_tickets,
|
||||||
)
|
)
|
||||||
from core.models.Result import Result, ResultError
|
|
||||||
from core.models.session.SessionProfile import SessionProfile
|
from core.models.session.SessionProfile import SessionProfile
|
||||||
from core.models.system.SystemProfile import SystemProfile
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||||
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||||
from gui.v2.actions.operation_results import result_from_exception
|
from gui.v2.actions.operation_result_dispatch import has_result_action
|
||||||
|
from gui.v2.actions.operation_results import result_from_payload
|
||||||
from gui.v2.actions.profile_order import normalize_profile_order
|
from gui.v2.actions.profile_order import normalize_profile_order
|
||||||
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
|
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
|
|
@ -79,8 +79,6 @@ class MenuPage(Page):
|
||||||
self.button_states = {}
|
self.button_states = {}
|
||||||
self.is_system_connected = False
|
self.is_system_connected = False
|
||||||
self.profile_button_map = {}
|
self.profile_button_map = {}
|
||||||
self._pending_operation_name = None
|
|
||||||
self._pending_operation_profile_data = None
|
|
||||||
self.font_style = f"font-family: '{main_window.open_sans_family}';" if main_window.open_sans_family else ""
|
self.font_style = f"font-family: '{main_window.open_sans_family}';" if main_window.open_sans_family else ""
|
||||||
|
|
||||||
self.create_interface_elements()
|
self.create_interface_elements()
|
||||||
|
|
@ -1259,8 +1257,6 @@ class MenuPage(Page):
|
||||||
self.enabling_profile(profile_data)
|
self.enabling_profile(profile_data)
|
||||||
|
|
||||||
def enabling_profile(self, profile_data):
|
def enabling_profile(self, profile_data):
|
||||||
self._pending_operation_name = 'enable_profile'
|
|
||||||
self._pending_operation_profile_data = dict(profile_data)
|
|
||||||
self.worker = Worker(profile_data)
|
self.worker = Worker(profile_data)
|
||||||
self.worker.update_signal.connect(self.update_gui_main_thread)
|
self.worker.update_signal.connect(self.update_gui_main_thread)
|
||||||
self.worker.change_page.connect(self.change_app_page)
|
self.worker.change_page.connect(self.change_app_page)
|
||||||
|
|
@ -1271,7 +1267,7 @@ class MenuPage(Page):
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def handle_operation_failure(self, payload):
|
def handle_operation_failure(self, payload):
|
||||||
result = self._result_from_payload(payload)
|
result = result_from_payload(payload)
|
||||||
if result.valid:
|
if result.valid:
|
||||||
if result.message:
|
if result.message:
|
||||||
self.update_status.update_status(result.message)
|
self.update_status.update_status(result.message)
|
||||||
|
|
@ -1284,7 +1280,7 @@ class MenuPage(Page):
|
||||||
self.disconnect_button.setEnabled(True)
|
self.disconnect_button.setEnabled(True)
|
||||||
self.disconnect_system_wide_button.setEnabled(True)
|
self.disconnect_system_wide_button.setEnabled(True)
|
||||||
|
|
||||||
is_actionable = result.error_type in self._operation_choices()
|
is_actionable = has_result_action(result)
|
||||||
|
|
||||||
self.popup = OperationResultPopup(
|
self.popup = OperationResultPopup(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1294,49 +1290,13 @@ class MenuPage(Page):
|
||||||
action_result=is_actionable,
|
action_result=is_actionable,
|
||||||
)
|
)
|
||||||
self.popup.action_selected.connect(
|
self.popup.action_selected.connect(
|
||||||
lambda accepted, current_result=result: self.handle_operation_popup_action(accepted, current_result))
|
lambda accepted: self.handle_operation_popup_action(accepted))
|
||||||
self.popup.show()
|
self.popup.show()
|
||||||
|
|
||||||
def _result_from_payload(self, payload):
|
def handle_operation_popup_action(self, accepted):
|
||||||
if isinstance(payload, Result):
|
if self.worker is None:
|
||||||
return payload
|
|
||||||
if isinstance(payload, Exception):
|
|
||||||
return result_from_exception(payload)
|
|
||||||
return Result(
|
|
||||||
valid=False,
|
|
||||||
error_type=ResultError.UNKNOWN,
|
|
||||||
message=str(payload) or 'The operation could not be completed.',
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_operation_popup_action(self, accepted, result):
|
|
||||||
if not accepted:
|
|
||||||
return
|
return
|
||||||
self._action_for_result(result)(result)
|
self.worker.handle_operation_popup_choice(accepted)
|
||||||
|
|
||||||
def _action_for_result(self, result):
|
|
||||||
return self._operation_choices().get(result.error_type, self._dismiss_operation_result)
|
|
||||||
|
|
||||||
def _operation_choices(self):
|
|
||||||
CHOICES = {
|
|
||||||
ResultError.CONNECTION: self._retry_pending_operation,
|
|
||||||
ResultError.SUBSCRIPTION: self._handle_expired_ticket_result,
|
|
||||||
}
|
|
||||||
return CHOICES
|
|
||||||
|
|
||||||
def _retry_pending_operation(self, result):
|
|
||||||
profile_data = self._pending_operation_profile_data
|
|
||||||
if not isinstance(profile_data, dict) or 'id' not in profile_data:
|
|
||||||
self.update_status.update_status('Retry is unavailable for this operation.')
|
|
||||||
return
|
|
||||||
self.update_status.update_status('Retrying...')
|
|
||||||
self.enabling_profile(dict(profile_data))
|
|
||||||
|
|
||||||
def _handle_expired_ticket_result(self, result):
|
|
||||||
self.update_status.update_status(result.message or 'Ticket expired.')
|
|
||||||
self._route_to_billing_entry()
|
|
||||||
|
|
||||||
def _dismiss_operation_result(self, result):
|
|
||||||
self.update_status.update_status(result.message or 'The operation could not be completed.')
|
|
||||||
|
|
||||||
def show_ticket_data_loss_popup(self, ticket_number, billing_code):
|
def show_ticket_data_loss_popup(self, ticket_number, billing_code):
|
||||||
self.custom_window.navigator.navigate("menu")
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from core.Errors import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||||
|
from gui.v2.actions.operation_result_dispatch import dispatch_result_action
|
||||||
from gui.v2.actions.operation_results import result_from_exception
|
from gui.v2.actions.operation_results import result_from_exception
|
||||||
from gui.v2.infrastructure.screen_size import get_max_screensize
|
from gui.v2.infrastructure.screen_size import get_max_screensize
|
||||||
from gui.v2.infrastructure.setup_observers import (
|
from gui.v2.infrastructure.setup_observers import (
|
||||||
|
|
@ -51,6 +52,8 @@ class Worker(QObject):
|
||||||
self.profile_type = None
|
self.profile_type = None
|
||||||
self._ticket_error_emitted = False
|
self._ticket_error_emitted = False
|
||||||
self._consumed_ticket = None
|
self._consumed_ticket = None
|
||||||
|
self._pending_operation_result = None
|
||||||
|
self._pending_operation_kwargs = {}
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -114,7 +117,7 @@ class Worker(QObject):
|
||||||
application_version_observer=application_version_observer,
|
application_version_observer=application_version_observer,
|
||||||
connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution)
|
connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution)
|
||||||
if isinstance(enable_result, Result) and not enable_result.valid:
|
if isinstance(enable_result, Result) and not enable_result.valid:
|
||||||
self.operation_failed.emit(enable_result)
|
self._emit_operation_failure(enable_result)
|
||||||
return
|
return
|
||||||
except EndpointVerificationError:
|
except EndpointVerificationError:
|
||||||
self.update_signal.emit(
|
self.update_signal.emit(
|
||||||
|
|
@ -247,8 +250,26 @@ class Worker(QObject):
|
||||||
error_type=ResultError.UNKNOWN,
|
error_type=ResultError.UNKNOWN,
|
||||||
message=message or 'Ticket use failed.',
|
message=message or 'Ticket use failed.',
|
||||||
)
|
)
|
||||||
|
self._emit_operation_failure(result, which_ticket=which_ticket)
|
||||||
|
|
||||||
|
def _emit_operation_failure(self, result, **kwargs):
|
||||||
|
self._pending_operation_result = result
|
||||||
|
self._pending_operation_kwargs = {
|
||||||
|
key: value for key, value in kwargs.items()
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
self.operation_failed.emit(result)
|
self.operation_failed.emit(result)
|
||||||
|
|
||||||
|
def handle_operation_popup_choice(self, accepted):
|
||||||
|
if not accepted:
|
||||||
|
return None
|
||||||
|
if not isinstance(self._pending_operation_result, Result):
|
||||||
|
return None
|
||||||
|
return dispatch_result_action(
|
||||||
|
self._pending_operation_result,
|
||||||
|
**self._pending_operation_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
def _emit_subscription_lookup_failure(self, exception=None):
|
def _emit_subscription_lookup_failure(self, exception=None):
|
||||||
message = (
|
message = (
|
||||||
"Ticket was accepted, but the subscription details could not be "
|
"Ticket was accepted, but the subscription details could not be "
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue