forked from Support/sp-hydra-veil-gui
update: updated ticket error handling
This commit is contained in:
parent
0cc496a40b
commit
8039ab08bb
6 changed files with 490 additions and 55 deletions
|
|
@ -775,11 +775,13 @@ class CustomWindow(QMainWindow):
|
||||||
if clear:
|
if clear:
|
||||||
self._set_status_font_size(16)
|
self._set_status_font_size(16)
|
||||||
self.status_label.setText('')
|
self.status_label.setText('')
|
||||||
|
self.disable_marquee()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
text = str(text)
|
||||||
full_text = 'Status: ' + text
|
full_text = 'Status: ' + text
|
||||||
metrics = self.status_label.fontMetrics()
|
metrics = self.status_label.fontMetrics()
|
||||||
available_width = self.status_label.width() - 10
|
available_width = getattr(self, 'text_width', self.status_label.width() - 10)
|
||||||
|
|
||||||
if metrics.horizontalAdvance(full_text) > available_width:
|
if metrics.horizontalAdvance(full_text) > available_width:
|
||||||
self.enable_marquee(text)
|
self.enable_marquee(text)
|
||||||
|
|
@ -809,10 +811,12 @@ class CustomWindow(QMainWindow):
|
||||||
self.navigator.navigate("menu")
|
self.navigator.navigate("menu")
|
||||||
|
|
||||||
def enable_marquee(self, text):
|
def enable_marquee(self, text):
|
||||||
self.marquee_text = text + " "
|
self.marquee_text = str(text)
|
||||||
|
self.marquee_gap = " "
|
||||||
self.marquee_position = 0
|
self.marquee_position = 0
|
||||||
self.marquee_enabled = True
|
self.marquee_enabled = True
|
||||||
self.marquee_timer.start(500)
|
self.update_marquee()
|
||||||
|
self.marquee_timer.start(180)
|
||||||
|
|
||||||
def disable_marquee(self):
|
def disable_marquee(self):
|
||||||
self.marquee_enabled = False
|
self.marquee_enabled = False
|
||||||
|
|
@ -823,31 +827,33 @@ class CustomWindow(QMainWindow):
|
||||||
return
|
return
|
||||||
|
|
||||||
metrics = self.status_label.fontMetrics()
|
metrics = self.status_label.fontMetrics()
|
||||||
text_width = metrics.horizontalAdvance(self.marquee_text)
|
prefix = 'Status: '
|
||||||
|
content_width = max(
|
||||||
|
80,
|
||||||
|
getattr(self, 'text_width', self.status_label.width() - 10)
|
||||||
|
- metrics.horizontalAdvance(prefix)
|
||||||
|
)
|
||||||
|
|
||||||
self.marquee_position += self.scroll_speed
|
marquee_unit = self.marquee_text + getattr(self, 'marquee_gap', " ")
|
||||||
if self.marquee_position >= text_width:
|
if not marquee_unit.strip():
|
||||||
|
self.disable_marquee()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.marquee_position >= len(marquee_unit):
|
||||||
self.marquee_position = 0
|
self.marquee_position = 0
|
||||||
|
|
||||||
looped_text = self.marquee_text * 2
|
looped_text = marquee_unit + marquee_unit
|
||||||
|
visible_text = ''
|
||||||
|
for char in looped_text[self.marquee_position:]:
|
||||||
|
next_text = visible_text + char
|
||||||
|
if metrics.horizontalAdvance(next_text) > content_width:
|
||||||
|
break
|
||||||
|
visible_text = next_text
|
||||||
|
|
||||||
chars_that_fit = metrics.horizontalAdvance(
|
display_text = prefix + visible_text
|
||||||
looped_text[:self.text_width])
|
|
||||||
|
|
||||||
visible_text = looped_text[self.marquee_position:
|
|
||||||
self.marquee_position + chars_that_fit]
|
|
||||||
|
|
||||||
while metrics.horizontalAdvance(visible_text) < self.text_width:
|
|
||||||
visible_text += self.marquee_text
|
|
||||||
|
|
||||||
while metrics.horizontalAdvance(visible_text) > self.text_width:
|
|
||||||
visible_text = visible_text[:-1]
|
|
||||||
|
|
||||||
display_text = 'Status: ' + ' ' * \
|
|
||||||
(self.text_start_x - metrics.horizontalAdvance('Status: '))
|
|
||||||
display_text += visible_text
|
|
||||||
|
|
||||||
self.status_label.setText(display_text)
|
self.status_label.setText(display_text)
|
||||||
|
self.marquee_position += max(1, int(self.scroll_speed / 7))
|
||||||
|
|
||||||
def set_scroll_speed(self, speed):
|
def set_scroll_speed(self, speed):
|
||||||
self.scroll_speed = speed
|
self.scroll_speed = speed
|
||||||
|
|
|
||||||
95
gui/v2/actions/operation_results.py
Executable file
95
gui/v2/actions/operation_results.py
Executable file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from core.models.Result import ResultError
|
||||||
|
|
||||||
|
|
||||||
|
def result_is_valid(result):
|
||||||
|
if hasattr(result, 'valid'):
|
||||||
|
return bool(result.valid)
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return bool(result.get('valid'))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def result_data(result):
|
||||||
|
if hasattr(result, 'data'):
|
||||||
|
return result.data
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return result.get('data') or result.get('billing_code')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def result_message(result, default='Operation failed.'):
|
||||||
|
if hasattr(result, 'message') and result.message:
|
||||||
|
return str(result.message)
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return str(result.get('message') or result.get('error_code') or default)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def result_error_value(result, default=ResultError.UNKNOWN.value):
|
||||||
|
error_type = None
|
||||||
|
if hasattr(result, 'error_type'):
|
||||||
|
error_type = result.error_type
|
||||||
|
elif isinstance(result, dict):
|
||||||
|
error_type = result.get('error_type') or result.get('error_code')
|
||||||
|
|
||||||
|
if error_type is None:
|
||||||
|
return default
|
||||||
|
if hasattr(error_type, 'value'):
|
||||||
|
return str(error_type.value)
|
||||||
|
return str(error_type)
|
||||||
|
|
||||||
|
|
||||||
|
def operation_failure_payload(operation, result=None, message=None, exception=None, context=None):
|
||||||
|
if exception is not None:
|
||||||
|
error_value = ResultError.UNKNOWN.value
|
||||||
|
detail = str(exception) or type(exception).__name__
|
||||||
|
else:
|
||||||
|
error_value = result_error_value(result)
|
||||||
|
detail = message or result_message(result)
|
||||||
|
|
||||||
|
title = _operation_title(operation, error_value)
|
||||||
|
return {
|
||||||
|
'operation': operation,
|
||||||
|
'title': title,
|
||||||
|
'message': detail,
|
||||||
|
'error_type': error_value,
|
||||||
|
'severity': _severity_for(error_value),
|
||||||
|
'actions': _actions_for(error_value),
|
||||||
|
'context': context or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _operation_title(operation, error_value):
|
||||||
|
if error_value == ResultError.CONNECTION.value:
|
||||||
|
return 'Connection Problem'
|
||||||
|
if operation == 'use_ticket' and error_value == ResultError.SUBSCRIPTION.value:
|
||||||
|
return 'Ticket Expired'
|
||||||
|
if operation == 'use_ticket':
|
||||||
|
return 'Ticket Use Failed'
|
||||||
|
if operation == 'enable_profile':
|
||||||
|
return 'Profile Enable Failed'
|
||||||
|
if operation == 'disable_profile':
|
||||||
|
return 'Profile Disable Failed'
|
||||||
|
return 'Operation Failed'
|
||||||
|
|
||||||
|
|
||||||
|
def _severity_for(error_value):
|
||||||
|
if error_value in {
|
||||||
|
ResultError.CONNECTION.value,
|
||||||
|
ResultError.SUBSCRIPTION.value,
|
||||||
|
ResultError.MISSING_FILE.value,
|
||||||
|
ResultError.MISSING_DATA.value,
|
||||||
|
}:
|
||||||
|
return 'warning'
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
|
||||||
|
def _actions_for(error_value):
|
||||||
|
if error_value == ResultError.CONNECTION.value:
|
||||||
|
return [
|
||||||
|
{'key': 'retry', 'label': 'Try Again', 'role': 'primary'},
|
||||||
|
{'key': 'cancel', 'label': 'Cancel', 'role': 'secondary'},
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
{'key': 'dismiss', 'label': 'OK', 'role': 'primary'},
|
||||||
|
]
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,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
|
||||||
|
|
@ -1258,10 +1259,50 @@ 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):
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
payload = {
|
||||||
|
'title': 'Operation Failed',
|
||||||
|
'message': str(payload),
|
||||||
|
'actions': [{'key': 'dismiss', 'label': 'OK', 'role': 'primary'}],
|
||||||
|
'severity': 'error',
|
||||||
|
'context': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
message = str(payload.get('message') or 'The operation could not be completed.')
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.popup = OperationResultPopup(
|
||||||
|
self,
|
||||||
|
title=payload.get('title', 'Operation Failed'),
|
||||||
|
message=message,
|
||||||
|
actions=payload.get('actions'),
|
||||||
|
severity=payload.get('severity', 'error'),
|
||||||
|
)
|
||||||
|
self.popup.action_selected.connect(
|
||||||
|
lambda action, current_payload=payload: self.handle_operation_popup_action(action, current_payload))
|
||||||
|
self.popup.show()
|
||||||
|
|
||||||
|
def handle_operation_popup_action(self, action, payload):
|
||||||
|
if action != 'retry':
|
||||||
|
return
|
||||||
|
context = payload.get('context') or {}
|
||||||
|
profile_data = context.get('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 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.')
|
||||||
|
|
|
||||||
223
gui/v2/ui/popups/operation_result_popup.py
Executable file
223
gui/v2/ui/popups/operation_result_popup.py
Executable file
|
|
@ -0,0 +1,223 @@
|
||||||
|
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_BG_QSS,
|
||||||
|
POPUP_CANCEL_BUTTON_QSS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OperationResultPopup(QDialog):
|
||||||
|
action_selected = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, title="", message="", actions=None, severity="error"):
|
||||||
|
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.actions = actions or [{'key': 'dismiss', 'label': 'OK', 'role': 'primary'}]
|
||||||
|
self.severity = severity
|
||||||
|
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(f"color: {self._title_color()}; 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('cancel'))
|
||||||
|
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()
|
||||||
|
for action in self.actions:
|
||||||
|
button = QPushButton(action.get('label', 'OK'))
|
||||||
|
button.setFixedSize(max(110, min(180, 22 + len(action.get('label', 'OK')) * 8)), 42)
|
||||||
|
button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
|
||||||
|
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
button.setStyleSheet(self._button_style(action.get('role', 'primary')))
|
||||||
|
button.clicked.connect(lambda checked=False, key=action.get('key', 'dismiss'): self._choose(key))
|
||||||
|
button_row.addWidget(button)
|
||||||
|
content_layout.addLayout(button_row)
|
||||||
|
|
||||||
|
if not self._use_parent_local_geometry:
|
||||||
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
|
|
||||||
|
def _title_color(self):
|
||||||
|
if self.severity == 'warning':
|
||||||
|
return '#d97706'
|
||||||
|
return '#d62828'
|
||||||
|
|
||||||
|
def _button_style(self, role):
|
||||||
|
if role == 'secondary':
|
||||||
|
return POPUP_CANCEL_BUTTON_QSS
|
||||||
|
if self.severity == 'warning':
|
||||||
|
return """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #f59e0b;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #d97706;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #d62828;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #b91c1c;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _choose(self, action):
|
||||||
|
if self._completed:
|
||||||
|
return
|
||||||
|
self._completed = True
|
||||||
|
self.action_selected.emit(action)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if not self._completed:
|
||||||
|
self._completed = True
|
||||||
|
self.action_selected.emit('cancel')
|
||||||
|
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()
|
||||||
|
|
@ -10,7 +10,7 @@ from core.controllers.tickets.UseTicketController import (
|
||||||
)
|
)
|
||||||
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 core.models.Result import Result, ResultError
|
from core.models.Result import ResultError
|
||||||
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
|
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
|
||||||
from core.Errors import (
|
from core.Errors import (
|
||||||
CommandNotFoundError,
|
CommandNotFoundError,
|
||||||
|
|
@ -25,6 +25,11 @@ 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_results import (
|
||||||
|
operation_failure_payload,
|
||||||
|
result_data,
|
||||||
|
result_is_valid,
|
||||||
|
)
|
||||||
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,
|
||||||
|
|
@ -38,6 +43,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
|
||||||
|
|
@ -82,18 +88,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:
|
||||||
subscription = SubscriptionController.get(
|
try:
|
||||||
self.profile_data['billing_code'], connection_observer=connection_observer)
|
subscription = SubscriptionController.get(
|
||||||
|
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
|
||||||
|
|
@ -113,8 +121,11 @@ class Worker(QObject):
|
||||||
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:
|
||||||
self.change_page.emit(
|
if self._consumed_ticket is not None:
|
||||||
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
|
self._emit_subscription_lookup_failure(exception=e)
|
||||||
|
else:
|
||||||
|
self.change_page.emit(
|
||||||
|
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
|
||||||
except ProfileActivationError:
|
except ProfileActivationError:
|
||||||
self.update_signal.emit(
|
self.update_signal.emit(
|
||||||
"The profile could not be enabled", False, None, None, None)
|
"The profile could not be enabled", False, None, None, None)
|
||||||
|
|
@ -196,11 +207,18 @@ 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={
|
||||||
|
'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
|
||||||
|
|
@ -211,16 +229,68 @@ class Worker(QObject):
|
||||||
location_id = getattr(location, 'id', None)
|
location_id = getattr(location, 'id', None)
|
||||||
if location_id is None:
|
if location_id is None:
|
||||||
return None
|
return None
|
||||||
location_id = str(location_id).strip()
|
if isinstance(location_id, str):
|
||||||
if not location_id:
|
location_id = location_id.strip()
|
||||||
|
if location_id == '':
|
||||||
return None
|
return None
|
||||||
return location_id
|
try:
|
||||||
|
return int(location_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _retry_profile_data(self, which_ticket=None):
|
||||||
|
profile_data = dict(self.profile_data)
|
||||||
|
profile_data.pop('billing_code', None)
|
||||||
|
if which_ticket is not None:
|
||||||
|
profile_data['use_ticket'] = which_ticket
|
||||||
|
return profile_data
|
||||||
|
|
||||||
|
def _emit_ticket_failure(self, which_ticket, result=None, message=None, exception=None):
|
||||||
|
self._ticket_error_emitted = True
|
||||||
|
context = {
|
||||||
|
'profile_data': self._retry_profile_data(which_ticket),
|
||||||
|
}
|
||||||
|
if which_ticket is not None:
|
||||||
|
context['ticket'] = str(which_ticket)
|
||||||
|
self.operation_failed.emit(
|
||||||
|
operation_failure_payload(
|
||||||
|
operation='use_ticket',
|
||||||
|
result=result,
|
||||||
|
message=message,
|
||||||
|
exception=exception,
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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={
|
||||||
|
'valid': False,
|
||||||
|
'error_type': ResultError.CONNECTION,
|
||||||
|
'message': message,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def _consume_ticket(self, which_ticket):
|
def _consume_ticket(self, which_ticket):
|
||||||
which_location = self._ticket_location_id()
|
which_location = self._ticket_location_id()
|
||||||
if which_location is None:
|
if which_location is None:
|
||||||
self._ticket_error_emitted = True
|
self._emit_ticket_failure(
|
||||||
self.change_page.emit('Could not determine profile location for ticket use.', True)
|
which_ticket,
|
||||||
|
result={
|
||||||
|
'valid': False,
|
||||||
|
'error_type': ResultError.MISSING_DATA,
|
||||||
|
'message': 'Could not determine profile location for ticket use.',
|
||||||
|
},
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -232,25 +302,25 @@ class Worker(QObject):
|
||||||
profile=self.profile
|
profile=self.profile
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._ticket_error_emitted = True
|
self._emit_ticket_failure(which_ticket, exception=e)
|
||||||
self.change_page.emit(f'Ticket use failed: {e}', True)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not isinstance(outcome, dict):
|
if result_is_valid(outcome):
|
||||||
self._ticket_error_emitted = True
|
billing_code = result_data(outcome)
|
||||||
self.change_page.emit('Ticket use failed: invalid_response', True)
|
if not billing_code:
|
||||||
return None
|
self._emit_ticket_failure(
|
||||||
|
which_ticket,
|
||||||
billing_code = outcome.get('billing_code')
|
result={
|
||||||
if outcome.get('valid') and billing_code:
|
'valid': False,
|
||||||
self._consumed_ticket = str(which_ticket)
|
'error_type': ResultError.INVALID_API_REPLY,
|
||||||
return billing_code
|
'message': 'Ticket use succeeded but no billing code was returned.',
|
||||||
if billing_code:
|
},
|
||||||
|
)
|
||||||
|
return None
|
||||||
self._consumed_ticket = str(which_ticket)
|
self._consumed_ticket = str(which_ticket)
|
||||||
return billing_code
|
return billing_code
|
||||||
|
|
||||||
self._ticket_error_emitted = True
|
self._emit_ticket_failure(which_ticket, result=outcome)
|
||||||
self.change_page.emit(f'Ticket use failed: {outcome.get("message", "failed")}', True)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_profile_status(self, profile, is_enabled):
|
def handle_profile_status(self, profile, is_enabled):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue