From 38535aee1c1091db82274ef84c5a2e861a1fc419 Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Thu, 22 Oct 2026 03:32:59 -0400 Subject: [PATCH] Ticket pages isolated to PyQt only, with calls to action functions that thread core --- gui/v2/actions/ticket_actions.py | 147 ++++++++--------- .../ui/pages/ticket_or_billing_choice_page.py | 11 +- gui/v2/ui/pages/ticket_prep_page.py | 148 ++++++++++-------- gui/v2/ui/styles/css/ticket_billing_styles.py | 13 +- 4 files changed, 161 insertions(+), 158 deletions(-) diff --git a/gui/v2/actions/ticket_actions.py b/gui/v2/actions/ticket_actions.py index fa7254e..47a4f0d 100644 --- a/gui/v2/actions/ticket_actions.py +++ b/gui/v2/actions/ticket_actions.py @@ -1,13 +1,11 @@ # core +from core.services.using_tickets.use_ticket_orchestrator import use_ticket_orchestrator +from core.controllers.tickets.TicketPrepController import prepare_tickets from core.controllers.ConfigurationController import ConfigurationController -from core.controllers.tickets.UseTicketController import use_ticket from core.services.prepare_tickets.ticket_tracker import get_a_random_ticket -from core.models.BaseProfile import BaseProfile as Profile -from core.controllers.ProfileController import ProfileController -# from core.controllers.tickets.TicketPayController import initiate_payment -from core.services.subscriptions.subscriptions_by_protocol import start_new_payment_cycle, save_subscription, get_ticket_invoice +from core.services.subscriptions.subscriptions_by_protocol import start_new_payment_cycle, get_ticket_invoice from core.services.payment_phase.ticket_cycle import wipe_ticket_cycle -# from core.services.payment_phase.payment_state import OverallPaymentCycle +from core.services.payment_phase.payment_state import TicketPaymentData from core.models.enum_models import PaymentCategory from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets from core.utils.type_checker import type_checker @@ -24,9 +22,24 @@ from gui.v2.actions.error_handlers import navigate_to_new_sub_page # generic: from typing import Callable +# from core.controllers.tickets.UseTicketController import use_ticket +# from core.models.BaseProfile import BaseProfile as Profile +# from core.controllers.ProfileController import ProfileController +# from core.controllers.tickets.TicketPayController import initiate_payment + + +@register_action(ResultError.TICKET_EXPIRED) +def wipe_ticket_billing() -> Result: + """Wipe existing data and start over.""" + wiped = wipe_ticket_cycle() + if wiped.valid: + return Result(valid=True, error_type=ResultError.NAVIGATE_TO, navigate_to="plan_picker") + else: + return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="plan_picker") + @register_action(ResultError.NO_TICKET) -def start_and_navigate_to_ticketing(): +def start_and_navigate_to_ticketing() -> Result: """Starts a new ticketing cycle. (assumes the user opted in).""" start_result = start_new_payment_cycle(category=PaymentCategory.TICKET) if start_result.valid: @@ -43,7 +56,6 @@ def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Resul User explicitly wants a random ticket. The reason this is in GUI repo, is because use_a_ticket is. """ - random_ticket_result = get_a_random_ticket() if not random_ticket_result.valid: @@ -55,8 +67,16 @@ def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Resul if not which_ticket: Result(valid=False, error_type=ResultError.NO_TICKET, message="No tickets left. Would you like to get some?") - # calls GUI repo function: - return use_a_ticket(profile_id=profile_id, which_ticket=which_ticket, ui_caller=passed_ui_caller) + # thread work that calls the caller, not return: + take_action( + action=use_ticket_orchestrator, + ui_caller=passed_ui_caller, + profile_id=profile_id, + which_ticket=which_ticket, + connection_observer=connection_observer, + ticket_observer=ticket_observer + ) + return Result(valid=True) @type_checker @@ -79,25 +99,44 @@ def dispatch_ticketing(profile_id: int, passed_ui_caller: Callable) -> Result: return navigate_to_new_sub_page() -def start_ticket(ui_caller: Callable) -> Result: +def start_ticket(ui_caller: Callable): + """Start the entire billing cycle for tickets""" logger.info("started ticketing billing state") notification = "Started Ticket Billing" Notifications.emit(EventType.STATUS_UPDATE, notification) - return take_action( + take_action( action=start_new_payment_cycle, category=PaymentCategory.TICKET, ui_caller=ui_caller ) -def fetch_ticket_invoice(ui_caller: Callable) -> str | None: - return take_action( +def fetch_ticket_invoice(ui_caller: Callable): + """Fetch the invoice for tickets""" + take_action( action=get_ticket_invoice, ui_caller=ui_caller, connection_observer=connection_observer ) +def start_bulk_ticket_prep(ui_caller: Callable) -> Result: + """After paid for, start the actual math""" + ticket_cycle = TicketPaymentData() + how_many_profiles = ticket_cycle.how_many_profiles + + return take_action( + action=prepare_tickets, + ui_caller=ui_caller, + how_many_profiles=how_many_profiles, + connection_observer=connection_observer, + ticket_observer=ticket_observer, + which_ticket=None, + billing_code=None + ) + + + def get_ticket_data() -> Result: """This is running in the background ALREADY. Get & format a list of unused tickets from core""" @@ -120,78 +159,14 @@ def get_ticket_data() -> Result: return Result(valid=True, data=formatted_data) -def use_a_ticket(profile_id: int, which_ticket: int, ui_caller: Callable) -> str | None: - """Coordinate the use of a given ticket. Returns a string on error, or None on success""" - try: - profile = ProfileController.get(int(profile_id)) - except Exception as e: - return f"The selected profile of {str(profile_id)} is either malformed or missing. {str(e)}" - - which_location = profile.location.id - protocol_choice = profile.connection.code - - PRE_REQS = [profile, which_ticket, which_location, protocol_choice, ui_caller] - if None in PRE_REQS: - return "You are missing one of the pre-reqs to use a ticket." - - ticket_user = TicketUser( - actual_profile=profile, +def use_a_ticket(profile_id: int, which_ticket: int, ui_caller: Callable): + """Use of a given ticket.""" + take_action( + action=use_ticket_orchestrator, + ui_caller=ui_caller, + profile_id=profile_id, which_ticket=which_ticket, - ui_caller=ui_caller + ticket_observer=ticket_observer, + connection_observer=connection_observer ) - ticket_user.begin_thread() - return None # no error - - -class TicketUser(): - actual_profile: Profile - which_ticket: int - ui_caller: Callable - - def begin_thread(self): - """Step 1, begin a background thread with use""" - notification = "Using Ticket.." - Notifications.emit(EventType.STATUS_UPDATE, notification) - take_action( - action=use_ticket, - ui_caller=self.apply_code, - which_location=self.which_location, - protocol_choice=self.protocol_choice, - ticket_observer=ticket_observer, - connection_observer=connection_observer, - ) - - def apply_code(self, result: Result): - """Step 2. Check if it worked. If so, apply it""" - - if not result.valid: - logger.error("Use failed, returning to UI layer..") - self.ui_caller(result) - - subscription = result.data - if not subscription: - logger.error("Use of ticket gave a blank subscription, returning to UI layer..") - self.ui_caller(result) - - # core saves it: - logger.info("saving subscription..") - saved = save_subscription(profile=self.actual_profile, subscription=subscription) - if not saved: - error_msg = "Could not save the subscription" - logger.error(error_msg) - new_result = Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg) - self.ui_caller(new_result) - - logger.info("Saved sub, returning to UI layer..") - self.ui_caller(result) - - -@register_action(ResultError.TICKET_EXPIRED) -def wipe_ticket_billing() -> Result: - """Wipe existing data and start over.""" - wiped = wipe_ticket_cycle() - if wiped.valid: - return Result(valid=True, error_type=ResultError.NAVIGATE_TO, navigate_to="plan_picker") - else: - return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="plan_picker") diff --git a/gui/v2/ui/pages/ticket_or_billing_choice_page.py b/gui/v2/ui/pages/ticket_or_billing_choice_page.py index 5db6c57..c8f8e0c 100755 --- a/gui/v2/ui/pages/ticket_or_billing_choice_page.py +++ b/gui/v2/ui/pages/ticket_or_billing_choice_page.py @@ -9,7 +9,7 @@ from gui.v2.actions.menu_tasks import enable_profile from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.ui.pages.Page import Page from gui.v2.infrastructure.take_plumbing_action import take_action -from gui.v2.actions.ticket_actions import get_ticket_data +from gui.v2.actions.ticket_actions import get_ticket_data, use_a_ticket # Bring background threads to the main thread UI: from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool @@ -125,17 +125,14 @@ class TicketOrBillingChoicePage(Page): self.update_status.update_status("Can not find the correct profile id") return - error_msg = use_a_ticket(profile_id=profile_id, which_ticket=which_ticket, ui_caller=self.recieve_use_result) - if error_msg: - self.show_a_popup(str(error_msg)) - else: - self.update_status.update_status(f"Using ticket #{which_ticket}...") + use_a_ticket(profile_id=profile_id, which_ticket=which_ticket, ui_caller=self.recieve_use_result) + self.update_status.update_status(f"Using ticket #{which_ticket}...") def recieve_use_result(self, result: Result): """Recieves the callback from gui_uses_ticket""" - if not result: + if not result or not isinstance(result, Result): self.show_a_popup("This is a developer error, the use of the ticket failed to produce a result object") if not result.valid: diff --git a/gui/v2/ui/pages/ticket_prep_page.py b/gui/v2/ui/pages/ticket_prep_page.py index 8b41a02..9c3da0f 100755 --- a/gui/v2/ui/pages/ticket_prep_page.py +++ b/gui/v2/ui/pages/ticket_prep_page.py @@ -1,11 +1,15 @@ -from PyQt6.QtWidgets import QLabel, QPushButton -from PyQt6 import QtCore +from core.models.Result import Result, ResultError +from gui.v2.actions.ticket_actions import start_bulk_ticket_prep +from gui.v2.ui.styles.css.ticket_billing_styles import TICKET_TERMINAL_STYLE, TICKET_TERMINAL_STYLE_SHEET from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.actions.menu_tasks import enable_profile from gui.v2.ui.pages.Page import Page from gui.v2.ui.widgets.terminal_widget import TerminalWidget -from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread +# from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread + +from PyQt6.QtWidgets import QLabel, QPushButton +from PyQt6 import QtCore HOW_MANY_PROFILES_DEFAULT = 6 @@ -18,7 +22,10 @@ class TicketPrepPage(Page): self.worker = None self._terminal_bound = False self._tickets_ready = False + self.setup_initial_layout() + self.start_prep() + def setup_initial_layout(self): self.title.setText("Preparing Tickets") self.title.setGeometry(QtCore.QRect(280, 20, 240, 40)) self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) @@ -28,18 +35,12 @@ class TicketPrepPage(Page): self.status_label = QLabel("", self) self.status_label.setGeometry(20, 460, 760, 30) - self.status_label.setStyleSheet("color: #00ffff; font-size: 14px;") + self.status_label.setStyleSheet(TICKET_TERMINAL_STYLE_SHEET) self.status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.continue_button = QPushButton("Continue", self) self.continue_button.setGeometry(340, 490, 120, 35) - self.continue_button.setStyleSheet(""" - QPushButton { - background-color: #2ecc71; color: white; border: none; - border-radius: 5px; font-size: 14px; font-weight: bold; - } - QPushButton:disabled { background-color: #555555; } - """) + self.continue_button.setStyleSheet(TICKET_TERMINAL_STYLE) self.continue_button.setEnabled(True) self.continue_button.clicked.connect(self.on_continue) @@ -61,60 +62,79 @@ class TicketPrepPage(Page): self.status_label.setText("Preparing tickets...") self.update_status.update_status("Preparing tickets...") - self.worker = TicketingWorkerThread('PREPARE_TICKETS', params={ - 'how_many_profiles': HOW_MANY_PROFILES_DEFAULT, - }) - self.worker.prep_done.connect(self.on_prep_done) - self.worker.error.connect(self.on_error) - self.worker.start() + # Actually Start it: + start_bulk_ticket_prep(ui_caller=self.on_prep_done) - def on_prep_done(self, result): - if not isinstance(result, dict): - self.terminal.append("ERROR: invalid result format.") - self.status_label.setText("Preparation failed.") - return - if result.get('valid') is True: - self._tickets_ready = True - self.update_status.clear_ticket_verification_failure() - self.terminal.append("=== Ticket preparation complete ===") - self.status_label.setText("Tickets ready. Click Continue to apply one to your profile.") - self.update_status.update_status("Tickets ready.") - self.continue_button.setEnabled(True) - else: - msg = result.get('message', 'failed') - self.terminal.append(f"ERROR: {msg}") - if msg == 'verification_failed': - how_many_failed = result.get('how_many_failed', '?') - failed_validations = result.get('failed_validations', []) - self.terminal.append(f" failed count: {how_many_failed}") - self.terminal.append(f" failed indices: {failed_validations}") - if self.update_status.save_ticket_verification_failure(result): - self.terminal.append("Recovery data saved. Open Settings > Tickets to evaluate or resume.") - self.status_label.setText("Verification failed. Open Settings > Tickets for recovery.") - self.update_status.update_status("Ticket verification failed") - self.continue_button.setEnabled(True) - return - self.status_label.setText(f"Preparation failed: {msg}") - self.update_status.update_status(f"An error occurred") - self.continue_button.setEnabled(True) - - def on_error(self, msg): - self.terminal.append(f"EXCEPTION: {msg}") - self.status_label.setText(f"An unknown error occured") + def on_prep_done(self, result: Result): self.continue_button.setEnabled(True) + if not result or not isinstance(result, Result): + self.terminal.append("Results returned to the graphical display in an invalid format.") + return + + if result.valid: + self.terminal.append("=== Ticket preparation complete ===") + self.status_label.setText("Tickets ready. Click Continue to continue to the main page.") + self.update_status.update_status("Tickets ready.") + return + + self.terminal.append("Recovery data saved. Open Settings > Tickets to evaluate or resume.") + self.status_label.setText("Verification failed. Open Settings > Tickets for recovery.") + self.update_status.update_status("Ticket verification failed") + def on_continue(self): - menu_page = self.custom_window.navigator.get_cached("menu") - profile_id = getattr(self.update_status, 'current_profile_id', None) - if menu_page: - self.custom_window.navigator.navigate("menu") - if not self._tickets_ready: - self.update_status.update_status("Ticket preparation was not completed.") - return - if profile_id is None or menu_page is None: - self.update_status.update_status("Tickets ready. Random ticket use is ON.") - return - self.update_status.update_status("Applying random ticket to profile...") - self.profile_enable_worker = enable_profile( - {'id': int(profile_id)}, menu_page.handle_task_result, allow_uninstalled_tor=True, - ) + self.custom_window.navigator.navigate("menu") + + + + + + + +# self.status_label.setText("Preparation failed.") +# if not isinstance(result, dict): +# self.terminal.append("ERROR: invalid result format.") +# return + +# if result.get('valid') is True: +# self._tickets_ready = True +# self.update_status.clear_ticket_verification_failure() +# else: +# msg = result.get('message', 'failed') +# self.terminal.append(f"ERROR: {msg}") +# if msg == 'verification_failed': +# how_many_failed = result.get('how_many_failed', '?') +# failed_validations = result.get('failed_validations', []) +# self.terminal.append(f" failed count: {how_many_failed}") +# self.terminal.append(f" failed indices: {failed_validations}") +# if self.update_status.save_ticket_verification_failure(result): + +# self.terminal.append("Recovery data saved. Open Settings > Tickets to evaluate or resume.") +# self.status_label.setText("Verification failed. Open Settings > Tickets for recovery.") +# self.update_status.update_status("Ticket verification failed") +# self.continue_button.setEnabled(True) +# return +# self.status_label.setText(f"Preparation failed: {msg}") +# self.update_status.update_status(f"An error occurred") +# self.continue_button.setEnabled(True) + +# def on_error(self, msg): +# self.terminal.append(f"EXCEPTION: {msg}") +# self.status_label.setText(f"An unknown error occured") +# self.continue_button.setEnabled(True) + +# def on_continue(self): +# menu_page = self.custom_window.navigator.get_cached("menu") +# profile_id = getattr(self.update_status, 'current_profile_id', None) +# if menu_page: +# self.custom_window.navigator.navigate("menu") +# if not self._tickets_ready: +# self.update_status.update_status("Ticket preparation was not completed.") +# return +# if profile_id is None or menu_page is None: +# self.update_status.update_status("Tickets ready. Random ticket use is ON.") +# return +# self.update_status.update_status("Applying random ticket to profile...") +# self.profile_enable_worker = enable_profile( +# {'id': int(profile_id)}, menu_page.handle_task_result, allow_uninstalled_tor=True, +# ) diff --git a/gui/v2/ui/styles/css/ticket_billing_styles.py b/gui/v2/ui/styles/css/ticket_billing_styles.py index fa0fe2d..78a417a 100644 --- a/gui/v2/ui/styles/css/ticket_billing_styles.py +++ b/gui/v2/ui/styles/css/ticket_billing_styles.py @@ -27,4 +27,15 @@ USE_BILLING_BUTTON_STYLE = """ font-size: 14px; font-weight: bold; } QPushButton:hover { background-color: #003333; } -""" \ No newline at end of file +""" + + +TICKET_TERMINAL_STYLE = """ + QPushButton { + background-color: #2ecc71; color: white; border: none; + border-radius: 5px; font-size: 14px; font-weight: bold; + } + QPushButton:disabled { background-color: #555555; } +""" + +TICKET_TERMINAL_STYLE_SHEET = "color: #00ffff; font-size: 14px;" \ No newline at end of file