update: fixed location page buttons display

This commit is contained in:
JOhn 2026-09-15 08:23:12 -04:00
parent 5975b2d254
commit 5d2b6f2eac
11 changed files with 547 additions and 121 deletions

0
gui/v2/actions/disable_profiles.py Normal file → Executable file
View file

View file

@ -1,35 +0,0 @@
def location_candidates(profile, preferred=None):
candidates = []
seen = set()
def add(val):
if val is None:
return
s = str(val).strip().lower().replace(' ', '_')
if s and s not in seen:
seen.add(s)
candidates.append(s)
if preferred:
add(preferred)
sources = []
try:
if profile and profile.connection and profile.connection.location:
sources.append(profile.connection.location)
except Exception:
pass
try:
if profile and profile.location:
sources.append(profile.location)
except Exception:
pass
for source in sources:
add(getattr(source, 'id', None))
add(getattr(source, 'country_name', None))
add(getattr(source, 'name', None))
add(getattr(source, 'code', None))
add(getattr(source, 'country_code', None))
return candidates

View 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}

View file

@ -0,0 +1,18 @@
from core.models.Result import Result, ResultError
def result_from_exception(exception: Exception, error_type: ResultError = ResultError.UNKNOWN) -> Result:
message = str(exception) or type(exception).__name__
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.',
)

View file

@ -26,5 +26,5 @@ def create_top_section(parent, css_path):
'marquee_timer': marquee_timer, 'marquee_timer': marquee_timer,
'text_start_x': 15, 'text_start_x': 15,
'text_end_x': 420, 'text_end_x': 420,
'text_width': 405, 'text_width': 500,
} }

View file

@ -1,6 +1,6 @@
import os import os
from PyQt6.QtWidgets import QApplication, QLabel, QPushButton, QButtonGroup from PyQt6.QtWidgets import QApplication, QLabel, QPushButton, QButtonGroup, QScrollArea, QWidget
from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QFont, QTransform from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QFont, QTransform
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6 import QtCore from PyQt6 import QtCore
@ -20,6 +20,7 @@ class LocationPage(Page):
self.display.setGeometry(QtCore.QRect(5, 10, 390, 520)) self.display.setGeometry(QtCore.QRect(5, 10, 390, 520))
self.title.setGeometry(395, 40, 380, 40) self.title.setGeometry(395, 40, 380, 40)
self.title.setText("Pick a location") self.title.setText("Pick a location")
self._setup_scroll_area()
self.initial_display = QLabel(self) self.initial_display = QLabel(self)
self.initial_display.setGeometry(5, 22, 355, 485) self.initial_display.setGeometry(5, 22, 355, 485)
@ -70,13 +71,24 @@ class LocationPage(Page):
self.refresh_protocol_location_visibility() self.refresh_protocol_location_visibility()
def create_interface_elements(self, available_locations): def create_interface_elements(self, available_locations):
old_button_widget = self.scroll_area.takeWidget()
if old_button_widget is not None:
old_button_widget.deleteLater()
button_widget = QWidget()
button_widget.setStyleSheet("background: transparent;")
self._button_widget = button_widget
self.buttonGroup = QButtonGroup(self) self.buttonGroup = QButtonGroup(self)
self.buttons = [] self.buttons = []
for j, (object_type, icon_name, geometry) in enumerate(available_locations): for j, (object_type, icon_name, geometry) in enumerate(available_locations):
boton = object_type(self) boton = object_type(button_widget)
boton.setGeometry(*geometry) boton.setGeometry(
0,
0,
geometry[2],
geometry[3],
)
boton.setIconSize(boton.size()) boton.setIconSize(boton.size())
boton.setCheckable(True) boton.setCheckable(True)
@ -123,11 +135,61 @@ class LocationPage(Page):
boton.clicked.connect( boton.clicked.connect(
lambda checked, loc=icon_name: self.show_location(loc)) lambda checked, loc=icon_name: self.show_location(loc))
content_height = max(
(geometry[3] for _, _, geometry in available_locations),
default=0,
)
button_widget.setFixedSize(380, max(1, content_height))
self.scroll_area.setWidget(button_widget)
self._layout_visible_buttons()
def _setup_scroll_area(self):
self.scroll_area = QScrollArea(self)
self.scroll_area.setGeometry(405, 90, 385, 400)
self.scroll_area.setWidgetResizable(False)
self.scroll_area.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll_area.setStyleSheet("""
QScrollArea {
background: transparent;
border: none;
}
QScrollBar:vertical {
background: transparent;
border: none;
width: 10px;
margin: 0px 0px 0px 4px;
}
QScrollBar::handle:vertical {
background: #00ffff;
border-radius: 5px;
min-height: 20px;
margin: 1px 0px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
""")
def refresh_protocol_location_visibility(self): def refresh_protocol_location_visibility(self):
for button in self.buttons: for button in self.buttons:
location_key = getattr(button, 'location_icon_name', None) location_key = getattr(button, 'location_icon_name', None)
locations = self.connection_manager.get_location_info(location_key) locations = self.connection_manager.get_location_info(location_key)
button.setVisible(not locations or self._location_supports_selected_protocol(locations)) button.setVisible(not locations or self._location_supports_selected_protocol(locations))
self._layout_visible_buttons()
def _layout_visible_buttons(self):
visible_buttons = [button for button in self.buttons if not button.isHidden()]
for index, button in enumerate(visible_buttons):
button.move((index % 2) * 195, (index // 2) * 105)
if hasattr(self, '_button_widget'):
button_height = self.buttons[0].height() if self.buttons else 0
row_count = (len(visible_buttons) + 1) // 2
self._button_widget.setFixedSize(
380,
max(1, row_count * 105 - 30 + button_height),
)
def _current_protocol(self): def _current_protocol(self):
profile_data = self.update_status.read_data() profile_data = self.update_status.read_data()

View file

@ -22,6 +22,8 @@ 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_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
@ -29,6 +31,7 @@ from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
from gui.v2.ui.popups.operation_result_popup import OperationResultPopup
from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup
from gui.v2.workers.worker import Worker from gui.v2.workers.worker import Worker
from gui.v2.workers.worker_thread import WorkerThread from gui.v2.workers.worker_thread import WorkerThread
@ -1257,10 +1260,43 @@ class MenuPage(Page):
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)
self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup) self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup)
self.worker.operation_failed.connect(self.handle_operation_failure)
thread = threading.Thread(target=self.worker.run) thread = threading.Thread(target=self.worker.run)
thread.start() thread.start()
def handle_operation_failure(self, payload):
result = result_from_payload(payload)
if result.valid:
if result.message:
self.update_status.update_status(result.message)
return
message = result.message or result.user_message()
self.update_status.update_status(message)
self.boton_just.setEnabled(True)
self.boton_just_session.setEnabled(True)
self.disconnect_button.setEnabled(True)
self.disconnect_system_wide_button.setEnabled(True)
is_actionable = has_result_action(result)
self.popup = OperationResultPopup(
self,
message=message,
action_button_text="Yes" if is_actionable else "OK",
cancel_button_text="No" if is_actionable else None,
action_result=is_actionable,
)
self.popup.action_selected.connect(
lambda accepted: self.handle_operation_popup_action(accepted))
self.popup.show()
def handle_operation_popup_action(self, accepted):
if self.worker is None:
return
self.worker.handle_operation_popup_choice(accepted)
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")
self.update_status.update_status('Critical: invalid billing code from ticket.') self.update_status.update_status('Critical: invalid billing code from ticket.')

View file

@ -1,10 +1,8 @@
from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton
from PyQt6 import QtCore from PyQt6 import QtCore
from core.controllers.ProfileController import ProfileController
from core.controllers.tickets.UseTicketController import get_unused_tickets from core.controllers.tickets.UseTicketController import get_unused_tickets
from gui.v2.actions.locations import location_candidates
from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.infrastructure.setup_observers import ticket_observer
from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.Page import Page
@ -91,20 +89,14 @@ class TicketOrBillingChoicePage(Page):
self.status_label.setText("Pick a ticket from the list first.") self.status_label.setText("Pick a ticket from the list first.")
return return
which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole) which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole)
profile_id = self.update_status.current_profile_id
try: try:
profile = ProfileController.get(int(profile_id)) profile_id = int(self.update_status.current_profile_id)
except Exception: except (TypeError, ValueError):
profile = None self.status_label.setText("Could not determine profile.")
candidates = location_candidates(profile)
if not candidates:
self.status_label.setText("Could not determine profile location.")
return return
profile_data = { profile_data = {
'id': int(profile_id), 'id': profile_id,
'use_ticket': which_ticket, 'use_ticket': which_ticket
'ticket_location': candidates[0],
'profile': profile
} }
menu_page = self.custom_window.navigator.get_cached("menu") menu_page = self.custom_window.navigator.get_cached("menu")
if menu_page: if menu_page:

View file

@ -0,0 +1,200 @@
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QFont, QGuiApplication
from PyQt6.QtWidgets import (
QDialog,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from gui.v2.ui.styles.styles import (
POPUP_ACTION_BUTTON_RED_QSS,
POPUP_BG_QSS,
POPUP_CANCEL_BUTTON_QSS,
)
class OperationResultPopup(QDialog):
action_selected = pyqtSignal(bool)
def __init__(self, parent=None, message="", title="Operation Failed", action_button_text="Yes", cancel_button_text="No", action_result=True):
super().__init__(parent)
self.parent_window = parent
self.title_text = title or "Operation Failed"
self.message = message or "The operation could not be completed."
self.action_button_text = action_button_text
self.cancel_button_text = cancel_button_text
self.action_result = action_result
self._completed = False
self._use_parent_local_geometry = QGuiApplication.platformName().lower().startswith("wayland")
self.initUI()
def initUI(self):
self.setFixedSize(560, 300)
if self._use_parent_local_geometry:
if self.parent_window is not None:
self.setParent(self.parent_window)
self.setWindowFlags(Qt.WindowType.Widget)
else:
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
bg_widget = QWidget(self)
bg_widget.setStyleSheet(POPUP_BG_QSS)
outer.addWidget(bg_widget)
content_layout = QVBoxLayout(bg_widget)
content_layout.setContentsMargins(22, 14, 22, 18)
content_layout.setSpacing(12)
top_row = QHBoxLayout()
top_row.setContentsMargins(0, 0, 0, 0)
top_row.setSpacing(10)
title_label = QLabel(self.title_text)
title_label.setFont(QFont("Arial", 18, QFont.Weight.Bold))
title_label.setStyleSheet("color: #d62828; background: transparent; border: none;")
title_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
title_label.setMinimumWidth(0)
top_row.addWidget(title_label, 1)
close_button = QPushButton("X")
close_button.setFixedSize(36, 36)
close_button.setFont(QFont("Arial", 14, QFont.Weight.Bold))
close_button.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #444444;
font-size: 18px;
font-weight: 900;
border: none;
}
QPushButton:hover {
color: #d62828;
}
""")
close_button.clicked.connect(lambda: self._choose(False))
top_row.addWidget(close_button, 0, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop)
content_layout.addLayout(top_row)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setStyleSheet("background: transparent; border: none;")
content_layout.addWidget(scroll_area, 1)
scroll_content = QWidget()
scroll_content.setStyleSheet("background: transparent; border: none;")
scroll_layout = QVBoxLayout(scroll_content)
scroll_layout.setContentsMargins(0, 0, 0, 0)
message_label = QLabel(self.message)
message_label.setFont(QFont("Arial", 12))
message_label.setStyleSheet("color: #333333; background: transparent; border: none;")
message_label.setWordWrap(True)
message_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
message_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
scroll_layout.addWidget(message_label)
scroll_layout.addStretch()
scroll_area.setWidget(scroll_content)
button_row = QHBoxLayout()
button_row.addStretch()
if self.cancel_button_text is not None:
cancel_button = QPushButton(self.cancel_button_text)
cancel_button.setFixedSize(max(110, min(180, 22 + len(self.cancel_button_text) * 8)), 42)
cancel_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
cancel_button.setStyleSheet(POPUP_CANCEL_BUTTON_QSS)
cancel_button.clicked.connect(lambda: self._choose(False))
button_row.addWidget(cancel_button)
action_button = QPushButton(self.action_button_text)
action_button.setFixedSize(max(110, min(180, 22 + len(self.action_button_text) * 8)), 42)
action_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
action_button.setCursor(Qt.CursorShape.PointingHandCursor)
action_button.setStyleSheet(POPUP_ACTION_BUTTON_RED_QSS)
action_button.clicked.connect(lambda: self._choose(self.action_result))
button_row.addWidget(action_button)
content_layout.addLayout(button_row)
if not self._use_parent_local_geometry:
self.setWindowModality(Qt.WindowModality.ApplicationModal)
def _choose(self, accepted):
if self._completed:
return
self._completed = True
self.action_selected.emit(accepted)
self.accept()
def closeEvent(self, event):
if not self._completed:
self._completed = True
self.action_selected.emit(False)
event.accept()
def show(self):
self._center_on_parent()
super().show()
def showEvent(self, event):
super().showEvent(event)
self._center_on_parent()
QTimer.singleShot(0, self._center_on_parent)
def _center_on_parent(self):
parent = self.parent_window
if parent is None:
return
try:
if not hasattr(parent, 'mapToGlobal') or not hasattr(parent, 'rect'):
return
parent_local_rect = parent.rect()
if parent_local_rect.isEmpty():
return
my_size = self.size()
if my_size.isEmpty() or my_size.width() <= 0 or my_size.height() <= 0:
my_size = self.sizeHint()
if self._use_parent_local_geometry:
target_x = parent_local_rect.x() + (parent_local_rect.width() - my_size.width()) // 2
target_y = parent_local_rect.y() + (parent_local_rect.height() - my_size.height()) // 2
target_x = max(parent_local_rect.left(), min(target_x, parent_local_rect.right() - my_size.width() + 1))
target_y = max(parent_local_rect.top(), min(target_y, parent_local_rect.bottom() - my_size.height() + 1))
else:
parent_center = parent.mapToGlobal(parent_local_rect.center())
target_x = parent_center.x() - my_size.width() // 2
target_y = parent_center.y() - my_size.height() // 2
screen = None
if hasattr(parent, 'screen'):
try:
screen = parent.screen()
except Exception:
screen = None
if screen is not None:
avail = screen.availableGeometry()
target_x = max(avail.left(), min(target_x, avail.right() - my_size.width()))
target_y = max(avail.top(), min(target_y, avail.bottom() - my_size.height()))
self.setGeometry(target_x, target_y, my_size.width(), my_size.height())
if self._use_parent_local_geometry:
self.raise_()
self.setFocus(Qt.FocusReason.PopupFocusReason)
except Exception:
pass
def mousePressEvent(self, event):
self.oldPos = event.globalPosition().toPoint()
def mouseMoveEvent(self, event):
delta = event.globalPosition().toPoint() - self.oldPos
self.move(self.x() + delta.x(), self.y() + delta.y())
self.oldPos = event.globalPosition().toPoint()

View file

@ -7,7 +7,6 @@ from core.controllers.tickets.FailedVerificationController import (
evaluate_if_its_the_key, evaluate_if_its_the_key,
prepare_tickets_with_saved_blind_sigs, prepare_tickets_with_saved_blind_sigs,
) )
from core.controllers.tickets.UseTicketController import use_ticket
from gui.v2.infrastructure.setup_observers import ( from gui.v2.infrastructure.setup_observers import (
connection_observer, connection_observer,
@ -22,7 +21,6 @@ class TicketingWorkerThread(QThread):
not_paid = pyqtSignal() not_paid = pyqtSignal()
paid_check_failed = pyqtSignal(str) paid_check_failed = pyqtSignal(str)
prep_done = pyqtSignal(object) prep_done = pyqtSignal(object)
use_done = pyqtSignal(object)
failed_verification_evaluated = pyqtSignal(object) failed_verification_evaluated = pyqtSignal(object)
saved_blind_prep_done = pyqtSignal(object) saved_blind_prep_done = pyqtSignal(object)
error = pyqtSignal(str) error = pyqtSignal(str)
@ -71,14 +69,5 @@ class TicketingWorkerThread(QThread):
elif self.action == 'PREPARE_SAVED_BLIND_SIGS': elif self.action == 'PREPARE_SAVED_BLIND_SIGS':
result = prepare_tickets_with_saved_blind_sigs(ticket_observer, connection_observer) result = prepare_tickets_with_saved_blind_sigs(ticket_observer, connection_observer)
self.saved_blind_prep_done.emit(result) self.saved_blind_prep_done.emit(result)
elif self.action == 'USE_TICKET':
result = use_ticket(
which_ticket=self.params['which_ticket'],
which_location=self.params['which_location'],
ticket_observer=ticket_observer,
connection_observer=connection_observer,
profile=self.params['profile']
)
self.use_done.emit(result)
except Exception as e: except Exception as e:
self.error.emit(str(e)) self.error.emit(str(e))

View file

@ -24,8 +24,9 @@ from core.Errors import (
UnsupportedApplicationVersionError, UnsupportedApplicationVersionError,
) )
from gui.v2.actions.locations import location_candidates
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.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 (
application_version_observer, application_version_observer,
@ -39,6 +40,7 @@ class Worker(QObject):
update_signal = pyqtSignal(str, bool, int, int, str) update_signal = pyqtSignal(str, bool, int, int, str)
change_page = pyqtSignal(str, bool) change_page = pyqtSignal(str, bool)
ticket_data_loss = pyqtSignal(str, str) ticket_data_loss = pyqtSignal(str, str)
operation_failed = pyqtSignal(object)
def __init__(self, profile_data): def __init__(self, profile_data):
self.profile_data = profile_data self.profile_data = profile_data
@ -50,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:
@ -70,8 +74,7 @@ class Worker(QObject):
if 'use_ticket' in self.profile_data: if 'use_ticket' in self.profile_data:
ticket_billing_code = self._consume_ticket( ticket_billing_code = self._consume_ticket(
self.profile_data['use_ticket'], self.profile_data['use_ticket'])
self.profile_data.get('ticket_location'))
if ticket_billing_code is None: if ticket_billing_code is None:
return return
self.profile_data['billing_code'] = ticket_billing_code self.profile_data['billing_code'] = ticket_billing_code
@ -84,18 +87,20 @@ class Worker(QObject):
self.profile_data['billing_code'] = ticket_billing_code self.profile_data['billing_code'] = ticket_billing_code
if 'billing_code' in self.profile_data: if 'billing_code' in self.profile_data:
try:
subscription = SubscriptionController.get( subscription = SubscriptionController.get(
self.profile_data['billing_code'], connection_observer=connection_observer) self.profile_data['billing_code'], connection_observer=connection_observer)
except Exception as e:
if self._consumed_ticket is not None:
self._emit_subscription_lookup_failure(exception=e)
return
subscription = None
if subscription is not None: if subscription is not None:
ProfileController.attach_subscription( ProfileController.attach_subscription(
self.profile, subscription) self.profile, subscription)
else: else:
if self._consumed_ticket is not None: if self._consumed_ticket is not None:
self._ticket_error_emitted = True self._emit_subscription_lookup_failure()
self.ticket_data_loss.emit(
self._consumed_ticket,
str(self.profile_data.get('billing_code', '')),
)
return return
self.change_page.emit('The billing code is invalid.', True) self.change_page.emit('The billing code is invalid.', True)
return return
@ -108,13 +113,19 @@ class Worker(QObject):
ignore_exceptions.append(ProfileStateConflictError) ignore_exceptions.append(ProfileStateConflictError)
ignore_tuple = tuple(ignore_exceptions) ignore_tuple = tuple(ignore_exceptions)
max_resolution = get_max_screensize() max_resolution = get_max_screensize()
ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer, enable_result = ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
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:
self._emit_operation_failure(enable_result)
return
except EndpointVerificationError: except EndpointVerificationError:
self.update_signal.emit( self.update_signal.emit(
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None) "ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
except (InvalidSubscriptionError, MissingSubscriptionError) as e: except (InvalidSubscriptionError, MissingSubscriptionError) as e:
if self._consumed_ticket is not None:
self._emit_subscription_lookup_failure(exception=e)
else:
self.change_page.emit( self.change_page.emit(
f"Subscription missing or invalid for profile {self.profile_data['id']}", True) f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
except ProfileActivationError: except ProfileActivationError:
@ -170,9 +181,6 @@ class Worker(QObject):
return None return None
def _location_candidates(self, preferred=None):
return location_candidates(self.profile, preferred)
def _profile_has_valid_subscription(self): def _profile_has_valid_subscription(self):
try: try:
sub = getattr(self.profile, 'subscription', None) sub = getattr(self.profile, 'subscription', None)
@ -201,55 +209,137 @@ class Worker(QObject):
try: try:
print("using a random ticket from GUI...") print("using a random ticket from GUI...")
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer) which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
except Exception: except Exception as e:
self._emit_ticket_failure(None, exception=e)
return None return None
if error_msg: if error_msg:
self._ticket_error_emitted = True self._emit_ticket_failure(
self.change_page.emit(f'Ticket use failed: {error_msg}', True) None,
result=Result(
valid=False,
error_type=ResultError.TICKET,
message=f'Ticket use failed: {error_msg}',
),
)
return None return None
if which_ticket is None or which_ticket == 'error': if which_ticket is None or which_ticket == 'error':
return None return None
return self._consume_ticket(which_ticket, None) return self._consume_ticket(which_ticket)
def _consume_ticket(self, which_ticket, which_location): def _ticket_location_id(self):
candidates = self._location_candidates(preferred=which_location) location = getattr(self.profile, 'location', None)
if not candidates: location_id = getattr(location, 'id', None)
self._ticket_error_emitted = True if location_id is None:
self.change_page.emit('Could not determine profile location for ticket use.', True) return None
if isinstance(location_id, str):
location_id = location_id.strip()
if location_id == '':
return None
try:
return int(location_id)
except (TypeError, ValueError):
return None
def _emit_ticket_failure(self, which_ticket, result=None, message=None, exception=None):
self._ticket_error_emitted = True
if exception is not None:
result = result_from_exception(exception)
elif result is None:
result = Result(
valid=False,
error_type=ResultError.UNKNOWN,
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)
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):
message = (
"Ticket was accepted, but the subscription details could not be "
"retrieved. Check your connection and try again."
)
if exception is not None:
error_text = str(exception)
if error_text:
message = f"{message} {error_text}"
self._emit_ticket_failure(
self._consumed_ticket,
result=Result(
valid=False,
error_type=ResultError.CONNECTION,
message=message,
),
)
def _consume_ticket(self, which_ticket):
which_location = self._ticket_location_id()
if which_location is None:
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.MISSING_DATA,
message='Could not determine profile location for ticket use.',
),
)
return None return None
last_msg = None
for cand in candidates:
try: try:
outcome = use_ticket( outcome = use_ticket(
which_ticket=which_ticket, which_ticket=which_ticket,
which_location=cand, which_location=which_location,
ticket_observer=ticket_observer, ticket_observer=ticket_observer,
connection_observer=connection_observer, connection_observer=connection_observer,
profile=self.profile profile=self.profile
) )
except Exception as e: except Exception as e:
last_msg = str(e) self._emit_ticket_failure(which_ticket, exception=e)
continue
if not isinstance(outcome, dict):
last_msg = 'invalid_response'
continue
billing_code = outcome.get('billing_code')
if outcome.get('valid') and billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
if billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
msg = outcome.get('message', 'failed')
last_msg = msg
if msg != 'invalid_location':
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {msg}', True)
return None return None
self._ticket_error_emitted = True if not isinstance(outcome, Result):
self.change_page.emit(f'Ticket use failed: {last_msg or "no valid location"}', True) self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.INVALID_API_REPLY,
message=f'use_ticket returned {type(outcome).__name__}, expected Result.',
),
)
return None
if outcome.valid:
billing_code = outcome.data
if not billing_code:
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.INVALID_API_REPLY,
message='Ticket use succeeded but no billing code was returned.',
),
)
return None
self._consumed_ticket = str(which_ticket)
return billing_code
self._emit_ticket_failure(which_ticket, result=outcome)
return None return None
def handle_profile_status(self, profile, is_enabled): def handle_profile_status(self, profile, is_enabled):