From 47458d59acca50804d5d091f5a9725fb42ab41ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 16:55:35 +0100 Subject: [PATCH 01/13] update form table construction --- src/services/tour/tour_service.py | 76 +++++++++---- .../tour/tour_time_computing_service.py | 7 +- src/views/main_page/delivery_form_page.py | 105 +----------------- src/views/main_page/form/__init__.py | 1 + src/views/main_page/form/tours_table.py | 52 +++++++++ .../main_page/form/tours_table_column.py | 12 ++ .../form/tours_table_column_items/__init__.py | 9 ++ .../tours_table_column_item_address.py | 8 ++ .../tours_table_column_item_delivery_man.py | 75 +++++++++++++ .../tours_table_column_item_time.py | 78 +++++++++++++ 10 files changed, 304 insertions(+), 119 deletions(-) create mode 100644 src/views/main_page/form/__init__.py create mode 100644 src/views/main_page/form/tours_table.py create mode 100644 src/views/main_page/form/tours_table_column.py create mode 100644 src/views/main_page/form/tours_table_column_items/__init__.py create mode 100644 src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py create mode 100644 src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py create mode 100644 src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py diff --git a/src/services/tour/tour_service.py b/src/services/tour/tour_service.py index 374c18d..8319346 100644 --- a/src/services/tour/tour_service.py +++ b/src/services/tour/tour_service.py @@ -1,4 +1,5 @@ from typing import Dict, List, Optional, Tuple +from uuid import UUID from reactivex import Observable, combine_latest from reactivex.operators import map @@ -35,6 +36,8 @@ def __init__(self) -> None: self.__computed_tours = BehaviorSubject({}) self.__selected_delivery_request = BehaviorSubject(None) + self.__tour_requests.subscribe(lambda _: self.compute_tours()) + @property def tour_requests(self) -> Observable[Dict[TourID, TourRequest]]: return self.__tour_requests @@ -80,8 +83,8 @@ def all_tours(self) -> Observable[List[Tour]]: ) def clear(self) -> None: - self.__tour_requests.on_next([]) - self.__computed_tours.on_next([]) + self.__tour_requests.on_next({}) + self.__computed_tours.on_next({}) def get_tour_requests(self) -> List[TourRequest]: return self.__tour_requests.value @@ -104,16 +107,7 @@ def add_delivery_request( time_window (int): Time window for the delivery tour_id (TourID): ID of the tour to add the delivery to (same as DeliveryMan ID) """ - tour_request = self.__tour_requests.value.get(tour_id) - - if not tour_request: - tour_request = TourRequest( - id=tour_id, - deliveries={}, - delivery_man=DeliveryManService.instance().get_delivery_man(tour_id), - color=COLORS[len(self.__tour_requests.value) % len(COLORS)], - ) - self.__tour_requests.value[tour_request.id] = tour_request + tour_request = self.__get_or_create_tour_request(tour_id) delivery_request = DeliveryRequest( location=DeliveryLocationService.instance().find_delivery_location_from_position( @@ -126,9 +120,6 @@ def add_delivery_request( self.__tour_requests.on_next(self.__tour_requests.value) - # TODO: review how to compute tours when adding a delivery request - self.compute_tours() - return delivery_request def remove_delivery_request( @@ -158,13 +149,44 @@ def remove_delivery_request( if self.__selected_delivery_request.value == tour_request: self.__selected_delivery_request.on_next(None) - # TODO: review how to compute tours when adding a delivery request - self.compute_tours() - return delivery_request + def update_delivery_request_time_window( + self, delivery_request_id: DeliveryID, tour_id: TourID, time_window: int + ) -> None: + tour_request = self.__tour_requests.value[tour_id] + delivery_request = tour_request.deliveries[delivery_request_id] + + if delivery_request.time_window == time_window: + return + + delivery_request.time_window = time_window + + self.__tour_requests.on_next(self.__tour_requests.value) + + def update_delivery_request_delivery_man( + self, delivery_request_id: DeliveryID, tour_id: TourID, delivery_man_id: UUID + ) -> None: + if tour_id == delivery_man_id: + return + + tour_request = self.__tour_requests.value[tour_id] + delivery_request = tour_request.deliveries[delivery_request_id] + + del tour_request.deliveries[delivery_request_id] + + self.__get_or_create_tour_request(delivery_man_id).deliveries[ + delivery_request_id + ] = delivery_request + + self.__tour_requests.on_next(self.__tour_requests.value) + def compute_tours(self) -> None: """Compute the tours and publish the update.""" + if len(self.__tour_requests.value) == 0: + self.__computed_tours.on_next({}) + return + map = MapService.instance().get_map() tours_intersection_ids = { @@ -175,7 +197,9 @@ def compute_tours(self) -> None: computed_tours = { id: TourTimeComputingService.instance().get_computed_tour_from_route_ids( self.__tour_requests.value[id], tour_intersection_ids - ) if tour_intersection_ids else None + ) + if tour_intersection_ids + else None for (id, tour_intersection_ids) in tours_intersection_ids.items() } @@ -203,3 +227,17 @@ def load_tours(self, path: str) -> None: path (str): Path to the file """ self.__computed_tours.on_next(TourSavingService.instance().load_tours(path)) + + def __get_or_create_tour_request(self, tour_id: TourID) -> Tour: + tour_request = self.__tour_requests.value.get(tour_id) + + if not tour_request: + tour_request = TourRequest( + id=tour_id, + deliveries={}, + delivery_man=DeliveryManService.instance().get_delivery_man(tour_id), + color=COLORS[len(self.__tour_requests.value) % len(COLORS)], + ) + self.__tour_requests.value[tour_request.id] = tour_request + + return tour_request diff --git a/src/services/tour/tour_time_computing_service.py b/src/services/tour/tour_time_computing_service.py index e50b5d5..1c37554 100644 --- a/src/services/tour/tour_time_computing_service.py +++ b/src/services/tour/tour_time_computing_service.py @@ -31,7 +31,12 @@ def get_computed_tour_from_route_ids( return ComputedTour.create_from_request( tour_request=tour_request, - deliveries={delivery.id: delivery for delivery in self.__compute_time_for_deliveries(tour_request, segment_route)}, + deliveries={ + delivery.id: delivery + for delivery in self.__compute_time_for_deliveries( + tour_request, segment_route + ) + }, route=segment_route, ) diff --git a/src/views/main_page/delivery_form_page.py b/src/views/main_page/delivery_form_page.py index 154257a..a8c4f5e 100644 --- a/src/views/main_page/delivery_form_page.py +++ b/src/views/main_page/delivery_form_page.py @@ -20,10 +20,10 @@ ComputedTour, Delivery, DeliveryID, + DeliveryRequest, Tour, TourID, TourRequest, - DeliveryRequest, ) from src.services.command.command_service import CommandService from src.services.command.commands.remove_delivery_request_command import ( @@ -31,13 +31,14 @@ ) from src.services.delivery_man.delivery_man_service import DeliveryManService from src.services.tour.tour_service import TourService +from src.views.main_page.form.tours_table import ToursTable from src.views.ui import Button, Callout, Separator, Text, TextSize class DeliveryFormPage(Page): __delivery_man_control: QComboBox __time_window_control: QComboBox - __delivery_table: QTableWidget + __delivery_table: ToursTable def __init__(self): super().__init__() @@ -72,7 +73,8 @@ def __init__(self): DeliveryManService.instance().delivery_men.subscribe( self.__update_delivery_man_combobox ) - TourService.instance().all_tours.subscribe(self.__update_delivery_table) + # TourService.instance().all_tours.subscribe(self.__update_delivery_table) + TourService.instance().all_tours.subscribe(self.__delivery_table.update_content) def compute_tour(self): TourService.instance().compute_tours() @@ -152,13 +154,7 @@ def __build_delivery_table(self) -> QLayout: # Define components to be used in this screen layout = QVBoxLayout() - table = QTableWidget() - table.setColumnCount(4) - table.setHorizontalHeaderLabels( - ["Delivery Address", "Time", "Delivery Man", ""] - ) - table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) - table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + table = ToursTable() self.__delivery_table = table @@ -228,95 +224,6 @@ def __update_time_window_combobox(self, delivery_man: DeliveryMan) -> None: max(self.__time_window_control.findData(current_value), 0) ) - def __update_delivery_table(self, tours: List[TourRequest | ComputedTour]) -> None: - self.__delivery_table.setRowCount(0) - - self.table_rows = [ - (tour, delivery) for tour in tours for delivery in tour.deliveries.values() - ] - - for tour, delivery in self.table_rows: - row_position = self.__delivery_table.rowCount() - self.__delivery_table.insertRow(row_position) - - self.__delivery_table.setItem( - row_position, 0, QTableWidgetItem(delivery.location.segment.name) - ) - self.__delivery_table.setItem( - row_position, - 1, - self.__build_time_table_item(delivery), - ) - self.__delivery_table.setCellWidget( - row_position, 2, self.__build_delivery_man_table_item(tour) - ) - self.__delivery_table.setCellWidget( - row_position, - 3, - self.__build_actions_table_item(delivery, tour), - ) - - def select_delivery_request(row_index: int) -> None: - TourService.instance().select_delivery_request( - self.table_rows[row_index][1].location - ) - - self.__delivery_table.itemClicked.connect( - lambda: select_delivery_request(self.__delivery_table.currentRow()) - ) - - self.__delivery_table.clearSelection() - TourService.instance().select_delivery_request(None) - - def __build_delivery_man_table_item(self, tour: Tour) -> QWidget: - delivery_man_widget = QWidget() - delivery_man_layout = QHBoxLayout() - - delivery_man_label = QLabel(tour.delivery_man.name) - delivery_man_label.setStyleSheet( - f""" - background-color: {tour.color if isinstance(tour, ComputedTour) else "#222222"}; - border-radius: 5px; - color: white; - text-align: center; - font-weight: 500; - font-size: 12px; - """ - ) - delivery_man_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - delivery_man_label.setContentsMargins(6, 0, 6, 0) - - delivery_man_layout.setContentsMargins(2, 6, 2, 6) - delivery_man_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter) - delivery_man_layout.addWidget(delivery_man_label) - delivery_man_widget.setLayout(delivery_man_layout) - - return delivery_man_widget - - def __build_actions_table_item(self, delivery: Delivery, tour: Tour) -> QWidget: - actions_widget = QWidget() - actions_layout = QHBoxLayout() - - remove_btn = Button("Remove") - remove_btn.clicked.connect( - lambda _, delivery=delivery, tour=tour: self.remove_delivery_location( - delivery_request_id=delivery.id, tour_id=tour.id - ) - ) - - actions_layout.setContentsMargins(2, 2, 2, 2) - actions_layout.addWidget(remove_btn) - actions_widget.setLayout(actions_layout) - - return actions_widget - - def __build_time_table_item(self, delivery: Delivery) -> QWidget: - return QTableWidgetItem( - delivery.time.strftime("%H:%M") - if isinstance(delivery, ComputedDelivery) - else f"{delivery.time_window}:00 - {delivery.time_window + 1}:00" if isinstance(delivery, DeliveryRequest) else "ERROR" - ) - def __save_tour(self): selected_delivery_man: DeliveryMan = self.__delivery_man_control.currentData() selected_time_window: int = self.__time_window_control.currentData() diff --git a/src/views/main_page/form/__init__.py b/src/views/main_page/form/__init__.py new file mode 100644 index 0000000..d2f0106 --- /dev/null +++ b/src/views/main_page/form/__init__.py @@ -0,0 +1 @@ +from src.views.main_page.form.tours_table import ToursTable diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py new file mode 100644 index 0000000..63cff3f --- /dev/null +++ b/src/views/main_page/form/tours_table.py @@ -0,0 +1,52 @@ +from typing import Dict, List + +from PyQt6.QtWidgets import QLabel, QTableWidget + +from src.models.tour import Tour +from src.views.main_page.form.tours_table_column import ToursTableColumn +from src.views.main_page.form.tours_table_column_items import ( + ToursTableColumnItemAddress, + ToursTableColumnItemDeliveryMan, + ToursTableColumnItemTime, +) + + +class ToursTable(QTableWidget): + COLUMNS: List[ToursTableColumn] = [ + ToursTableColumn( + header="Adresse", + render=lambda tour, delivery: ToursTableColumnItemAddress(delivery), + ), + ToursTableColumn( + header="Heure", + render=ToursTableColumnItemTime, + ), + ToursTableColumn( + header="Livreur", + render=ToursTableColumnItemDeliveryMan, + ), + ] + + def __init__(self): + super().__init__() + self.__setup_table() + + def update_content(self, tours: List[Tour]) -> None: + self.setRowCount(0) + + for tour in tours: + for delivery in tour.deliveries.values(): + row = self.rowCount() + + self.insertRow(row) + + for column, column_factory in enumerate(self.COLUMNS): + self.setCellWidget( + row, column, column_factory.render(tour, delivery) + ) + + def __setup_table(self): + self.setColumnCount(len(self.COLUMNS)) + self.setHorizontalHeaderLabels([column.header for column in self.COLUMNS]) + self.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + self.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) diff --git a/src/views/main_page/form/tours_table_column.py b/src/views/main_page/form/tours_table_column.py new file mode 100644 index 0000000..6201279 --- /dev/null +++ b/src/views/main_page/form/tours_table_column.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Callable + +from PyQt6.QtWidgets import QWidget + +from src.models.tour import Delivery, Tour + + +@dataclass +class ToursTableColumn: + header: str + render: Callable[[Tour, Delivery], QWidget] diff --git a/src/views/main_page/form/tours_table_column_items/__init__.py b/src/views/main_page/form/tours_table_column_items/__init__.py new file mode 100644 index 0000000..fd65e57 --- /dev/null +++ b/src/views/main_page/form/tours_table_column_items/__init__.py @@ -0,0 +1,9 @@ +from src.views.main_page.form.tours_table_column_items.tours_table_column_item_address import ( + ToursTableColumnItemAddress, +) +from src.views.main_page.form.tours_table_column_items.tours_table_column_item_delivery_man import ( + ToursTableColumnItemDeliveryMan, +) +from src.views.main_page.form.tours_table_column_items.tours_table_column_item_time import ( + ToursTableColumnItemTime, +) diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py new file mode 100644 index 0000000..853a9a1 --- /dev/null +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py @@ -0,0 +1,8 @@ +from PyQt6.QtWidgets import QLabel + +from src.models.tour import Delivery + + +class ToursTableColumnItemAddress(QLabel): + def __init__(self, delivery: Delivery): + super().__init__(delivery.location.segment.name) diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py new file mode 100644 index 0000000..3de9986 --- /dev/null +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py @@ -0,0 +1,75 @@ +from typing import Dict +from uuid import UUID + +from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QWidget + +from src.models.delivery_man.delivery_man import DeliveryMan +from src.models.tour import Delivery, Tour +from src.services.delivery_man.delivery_man_service import DeliveryManService +from src.services.tour.tour_service import TourService + + +class ToursTableColumnItemDeliveryMan(QWidget): + __tour: Tour + __delivery: Delivery + __delivery_men_control: QComboBox + + def __init__(self, tour: Tour, delivery: Delivery): + super().__init__() + + self.__tour = tour + self.__delivery = delivery + + self.__build() + + delivery_man_subscription = ( + DeliveryManService.instance().delivery_men.subscribe( + self.__update_delivery_men_control_items + ) + ) + + self.destroyed.connect(lambda: delivery_man_subscription.dispose()) + + def __build(self): + self.__build_delivery_men_control() + + layout = QHBoxLayout() + layout.addWidget(self.__delivery_men_control) + layout.setContentsMargins(0, 0, 0, 0) + + self.setLayout(layout) + + def __build_delivery_men_control(self): + self.__delivery_men_control = QComboBox() + + def __update_delivery_men_control_items(self, delivery_men: Dict[int, DeliveryMan]): + self.__delivery_men_control.clear() + + try: + self.__delivery_men_control.currentIndexChanged.disconnect() + except TypeError: + pass + + for i, delivery_man in enumerate(delivery_men.values()): + self.__delivery_men_control.addItem( + delivery_man.name, userData=delivery_man.id + ) + + if self.__tour.delivery_man.id == delivery_man.id: + self.__delivery_men_control.setCurrentIndex(i) + + self.__delivery_men_control.currentIndexChanged.connect( + lambda: self.__handle_delivery_man_change( + self.__delivery_men_control.currentData() + ) + ) + + def __handle_delivery_man_change(self, delivery_man_id: UUID): + if delivery_man_id is None or delivery_man_id == self.__tour.delivery_man.id: + return + + TourService.instance().update_delivery_request_delivery_man( + delivery_request_id=self.__delivery.id, + tour_id=self.__tour.id, + delivery_man_id=delivery_man_id, + ) diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py new file mode 100644 index 0000000..44d0689 --- /dev/null +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py @@ -0,0 +1,78 @@ +from typing import Dict + +from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QWidget + +from src.models.delivery_man.delivery_man import DeliveryMan +from src.models.tour import ComputedDelivery, Delivery, Tour +from src.services.delivery_man.delivery_man_service import DeliveryManService +from src.services.tour.tour_service import TourService + + +class ToursTableColumnItemTime(QWidget): + __tour: Tour + __delivery: Delivery + __time_control: QComboBox + + def __init__(self, tour: Tour, delivery: Delivery): + super().__init__() + + self.__tour = tour + self.__delivery = delivery + + self.__build() + + delivery_man_subscription = ( + DeliveryManService.instance().delivery_men.subscribe( + lambda _: self.__update_time_control_items() + ) + ) + + self.destroyed.connect(lambda: delivery_man_subscription.dispose()) + + def __build(self): + self.__build_time_control() + + layout = QHBoxLayout() + layout.addWidget(self.__time_control) + layout.setContentsMargins(0, 0, 0, 0) + + self.setLayout(layout) + + def __build_time_control(self): + self.__time_control = QComboBox() + + def __update_time_control_items(self): + self.__time_control.clear() + + try: + self.__time_control.currentIndexChanged.disconnect() + except TypeError: + pass + + for i, availability in enumerate(self.__tour.delivery_man.availabilities): + if ( + isinstance(self.__delivery, ComputedDelivery) + and self.__delivery.time.hour == availability + ): + self.__time_control.addItem( + self.__delivery.time.strftime("%H:%M"), userData=-1 + ) + self.__time_control.setCurrentIndex(i) + else: + self.__time_control.addItem( + f"{availability}:00 - {availability + 1}:00", userData=availability + ) + + self.__time_control.currentIndexChanged.connect( + lambda: self.__handle_time_change(self.__time_control.currentData()) + ) + + def __handle_time_change(self, time_window: int): + if time_window is None or time_window == -1: + return + + TourService.instance().update_delivery_request_time_window( + delivery_request_id=self.__delivery.id, + tour_id=self.__tour.id, + time_window=time_window, + ) From e69c51503a3e760e6502a842e3a2cd797d57ad0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:02:26 +0100 Subject: [PATCH 02/13] added actions to new table --- src/views/main_page/delivery_form_page.py | 13 ------- src/views/main_page/form/tours_table.py | 5 +++ .../form/tours_table_column_items/__init__.py | 3 ++ .../tours_table_column_item_actions.py | 37 +++++++++++++++++++ 4 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 src/views/main_page/form/tours_table_column_items/tours_table_column_item_actions.py diff --git a/src/views/main_page/delivery_form_page.py b/src/views/main_page/delivery_form_page.py index a8c4f5e..fed3e71 100644 --- a/src/views/main_page/delivery_form_page.py +++ b/src/views/main_page/delivery_form_page.py @@ -68,27 +68,14 @@ def __init__(self): self.setLayout(layout) - # UNUSED self.address_list = [] - DeliveryManService.instance().delivery_men.subscribe( self.__update_delivery_man_combobox ) - # TourService.instance().all_tours.subscribe(self.__update_delivery_table) TourService.instance().all_tours.subscribe(self.__delivery_table.update_content) def compute_tour(self): TourService.instance().compute_tours() - def remove_delivery_location( - self, delivery_request_id: DeliveryID, tour_id: TourID - ): - CommandService.instance().execute( - RemoveDeliveryRequestCommand( - delivery_request_id=delivery_request_id, - tour_id=tour_id, - ) - ) - def __build_warehouse_location(self) -> QLayout: # Define components to be used in this screen layout = QHBoxLayout() diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py index 63cff3f..f767d6c 100644 --- a/src/views/main_page/form/tours_table.py +++ b/src/views/main_page/form/tours_table.py @@ -5,6 +5,7 @@ from src.models.tour import Tour from src.views.main_page.form.tours_table_column import ToursTableColumn from src.views.main_page.form.tours_table_column_items import ( + ToursTableColumnItemActions, ToursTableColumnItemAddress, ToursTableColumnItemDeliveryMan, ToursTableColumnItemTime, @@ -25,6 +26,10 @@ class ToursTable(QTableWidget): header="Livreur", render=ToursTableColumnItemDeliveryMan, ), + ToursTableColumn( + header="", + render=ToursTableColumnItemActions, + ), ] def __init__(self): diff --git a/src/views/main_page/form/tours_table_column_items/__init__.py b/src/views/main_page/form/tours_table_column_items/__init__.py index fd65e57..f01f438 100644 --- a/src/views/main_page/form/tours_table_column_items/__init__.py +++ b/src/views/main_page/form/tours_table_column_items/__init__.py @@ -1,3 +1,6 @@ +from src.views.main_page.form.tours_table_column_items.tours_table_column_item_actions import ( + ToursTableColumnItemActions, +) from src.views.main_page.form.tours_table_column_items.tours_table_column_item_address import ( ToursTableColumnItemAddress, ) diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_actions.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_actions.py new file mode 100644 index 0000000..046bfd9 --- /dev/null +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_actions.py @@ -0,0 +1,37 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QHBoxLayout, QWidget + +from src.models.tour import Delivery, Tour +from src.services.command.command_service import CommandService +from src.services.command.commands.remove_delivery_request_command import ( + RemoveDeliveryRequestCommand, +) +from src.views.ui import Button + + +class ToursTableColumnItemActions(QWidget): + __tour: Tour + __delivery: Delivery + + def __init__(self, tour: Tour, delivery: Delivery): + super().__init__() + + self.__tour = tour + self.__delivery = delivery + + layout = QHBoxLayout(self) + layout.setContentsMargins(2, 2, 2, 2) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + remove_btn = Button(icon="trash") + remove_btn.clicked.connect(self.__handle_remove_click) + + layout.addWidget(remove_btn) + + def __handle_remove_click(self): + CommandService.instance().execute( + RemoveDeliveryRequestCommand( + delivery_request_id=self.__delivery.id, + tour_id=self.__tour.id, + ) + ) From b91cabe58aca94c88509f081c6210864fbefb140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:09:53 +0100 Subject: [PATCH 03/13] added tour color to row --- src/views/main_page/form/tours_table.py | 10 +++++++++ .../main_page/form/tours_table_column.py | 3 ++- .../form/tours_table_column_items/__init__.py | 3 +++ .../tours_table_column_item_tour.py | 22 +++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/views/main_page/form/tours_table_column_items/tours_table_column_item_tour.py diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py index f767d6c..0527a5b 100644 --- a/src/views/main_page/form/tours_table.py +++ b/src/views/main_page/form/tours_table.py @@ -9,11 +9,17 @@ ToursTableColumnItemAddress, ToursTableColumnItemDeliveryMan, ToursTableColumnItemTime, + ToursTableColumnItemTour, ) class ToursTable(QTableWidget): COLUMNS: List[ToursTableColumn] = [ + ToursTableColumn( + header="Tour", + render=ToursTableColumnItemTour, + width=40, + ), ToursTableColumn( header="Adresse", render=lambda tour, delivery: ToursTableColumnItemAddress(delivery), @@ -55,3 +61,7 @@ def __setup_table(self): self.setHorizontalHeaderLabels([column.header for column in self.COLUMNS]) self.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + + for i, col in enumerate(self.COLUMNS): + if col.width is not None: + self.setColumnWidth(i, col.width) diff --git a/src/views/main_page/form/tours_table_column.py b/src/views/main_page/form/tours_table_column.py index 6201279..f3c81c0 100644 --- a/src/views/main_page/form/tours_table_column.py +++ b/src/views/main_page/form/tours_table_column.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Callable +from typing import Callable, Optional from PyQt6.QtWidgets import QWidget @@ -10,3 +10,4 @@ class ToursTableColumn: header: str render: Callable[[Tour, Delivery], QWidget] + width: Optional[int] = None diff --git a/src/views/main_page/form/tours_table_column_items/__init__.py b/src/views/main_page/form/tours_table_column_items/__init__.py index f01f438..a4d8257 100644 --- a/src/views/main_page/form/tours_table_column_items/__init__.py +++ b/src/views/main_page/form/tours_table_column_items/__init__.py @@ -10,3 +10,6 @@ from src.views.main_page.form.tours_table_column_items.tours_table_column_item_time import ( ToursTableColumnItemTime, ) +from src.views.main_page.form.tours_table_column_items.tours_table_column_item_tour import ( + ToursTableColumnItemTour, +) diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_tour.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_tour.py new file mode 100644 index 0000000..948977c --- /dev/null +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_tour.py @@ -0,0 +1,22 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QHBoxLayout, QLabel, QWidget + +from src.models.tour import Delivery, Tour + + +class ToursTableColumnItemTour(QWidget): + def __init__(self, tour: Tour, delivery: Delivery): + super().__init__() + + layout = QHBoxLayout(self) + layout.setContentsMargins(2, 2, 2, 2) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + tour_item = QLabel(" ") + tour_item.setStyleSheet( + f""" + background-color: {tour.color}; + """ + ) + + layout.addWidget(tour_item) From 1fb1aa5daaaaf7c09f8154db047dbc5aa7fb75ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:10:21 +0100 Subject: [PATCH 04/13] simplify columns --- src/views/main_page/form/tours_table.py | 2 +- .../tours_table_column_item_address.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py index 0527a5b..252ca6c 100644 --- a/src/views/main_page/form/tours_table.py +++ b/src/views/main_page/form/tours_table.py @@ -22,7 +22,7 @@ class ToursTable(QTableWidget): ), ToursTableColumn( header="Adresse", - render=lambda tour, delivery: ToursTableColumnItemAddress(delivery), + render=ToursTableColumnItemAddress, ), ToursTableColumn( header="Heure", diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py index 853a9a1..0de2ae7 100644 --- a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py @@ -1,8 +1,8 @@ from PyQt6.QtWidgets import QLabel -from src.models.tour import Delivery +from src.models.tour import Tour, Delivery class ToursTableColumnItemAddress(QLabel): - def __init__(self, delivery: Delivery): + def __init__(self, tour: Tour, delivery: Delivery): super().__init__(delivery.location.segment.name) From d47cc308c1e1fcde554d217723d081323a318220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:11:49 +0100 Subject: [PATCH 05/13] adjust columns size --- src/views/main_page/form/tours_table.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py index 252ca6c..5fe0d00 100644 --- a/src/views/main_page/form/tours_table.py +++ b/src/views/main_page/form/tours_table.py @@ -23,6 +23,7 @@ class ToursTable(QTableWidget): ToursTableColumn( header="Adresse", render=ToursTableColumnItemAddress, + width=200, ), ToursTableColumn( header="Heure", @@ -31,10 +32,12 @@ class ToursTable(QTableWidget): ToursTableColumn( header="Livreur", render=ToursTableColumnItemDeliveryMan, + width=150, ), ToursTableColumn( header="", render=ToursTableColumnItemActions, + width=80 ), ] From a58c99571ef9fc63369225126b65fb2690139350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:17:36 +0100 Subject: [PATCH 06/13] convert update time window to command --- ...te_delivery_request_time_window_command.py | 39 +++++++++++++++++++ src/services/tour/tour_service.py | 5 ++- .../tour/tour_time_computing_service.py | 4 +- src/views/main_page/form/tours_table.py | 6 +-- .../tours_table_column_item_address.py | 2 +- .../tours_table_column_item_time.py | 18 +++++---- 6 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 src/services/command/commands/update_delivery_request_time_window_command.py diff --git a/src/services/command/commands/update_delivery_request_time_window_command.py b/src/services/command/commands/update_delivery_request_time_window_command.py new file mode 100644 index 0000000..4724a0c --- /dev/null +++ b/src/services/command/commands/update_delivery_request_time_window_command.py @@ -0,0 +1,39 @@ +from typing import Optional + +from src.models.tour import DeliveryID, TourID +from src.services.command.abstract_command import AbstractCommand +from src.services.tour.tour_service import TourService + + +class UpdateDeliveryRequestTimeWindow(AbstractCommand): + __delivery_request_id: DeliveryID + __tour_id: TourID + __time_window: int + __previous_time_window: Optional[int] = None + + def __init__( + self, delivery_request_id: DeliveryID, tour_id: TourID, time_window: int + ) -> None: + super().__init__("Ajustement de la fenĂȘtre de temps d'une demande de livraison") + self.__delivery_request_id = delivery_request_id + self.__tour_id = tour_id + self.__time_window = time_window + + def execute(self) -> None: + self.__previous_time_window = ( + TourService.instance().update_delivery_request_time_window( + delivery_request_id=self.__delivery_request_id, + tour_id=self.__tour_id, + time_window=self.__time_window, + ) + ) + + def undo(self) -> None: + if self.__time_window is None: + raise Exception("Cannot undo a command that has not been executed") + + TourService.instance().update_delivery_request_time_window( + delivery_request_id=self.__delivery_request_id, + tour_id=self.__tour_id, + time_window=self.__previous_time_window, + ) diff --git a/src/services/tour/tour_service.py b/src/services/tour/tour_service.py index 8319346..311ed1e 100644 --- a/src/services/tour/tour_service.py +++ b/src/services/tour/tour_service.py @@ -153,17 +153,20 @@ def remove_delivery_request( def update_delivery_request_time_window( self, delivery_request_id: DeliveryID, tour_id: TourID, time_window: int - ) -> None: + ) -> int: tour_request = self.__tour_requests.value[tour_id] delivery_request = tour_request.deliveries[delivery_request_id] if delivery_request.time_window == time_window: return + previous_time_window = delivery_request.time_window delivery_request.time_window = time_window self.__tour_requests.on_next(self.__tour_requests.value) + return previous_time_window + def update_delivery_request_delivery_man( self, delivery_request_id: DeliveryID, tour_id: TourID, delivery_man_id: UUID ) -> None: diff --git a/src/services/tour/tour_time_computing_service.py b/src/services/tour/tour_time_computing_service.py index 1c37554..f1ace30 100644 --- a/src/services/tour/tour_time_computing_service.py +++ b/src/services/tour/tour_time_computing_service.py @@ -64,8 +64,8 @@ def __compute_time_for_deliveries( if travel_time < time_window_start: travel_time = time_window_start - if travel_time > time_window_start + Config.TIME_WINDOW_SIZE: - raise Exception("Delivery time window exceeded") + # if travel_time > time_window_start + Config.TIME_WINDOW_SIZE: + # raise Exception("Delivery time window exceeded") computed_deliveries.append( ComputedDelivery.create_from_request( diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py index 5fe0d00..2c27226 100644 --- a/src/views/main_page/form/tours_table.py +++ b/src/views/main_page/form/tours_table.py @@ -34,11 +34,7 @@ class ToursTable(QTableWidget): render=ToursTableColumnItemDeliveryMan, width=150, ), - ToursTableColumn( - header="", - render=ToursTableColumnItemActions, - width=80 - ), + ToursTableColumn(header="", render=ToursTableColumnItemActions, width=80), ] def __init__(self): diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py index 0de2ae7..8e97080 100644 --- a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_address.py @@ -1,6 +1,6 @@ from PyQt6.QtWidgets import QLabel -from src.models.tour import Tour, Delivery +from src.models.tour import Delivery, Tour class ToursTableColumnItemAddress(QLabel): diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py index 44d0689..19c5a82 100644 --- a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py @@ -1,11 +1,11 @@ -from typing import Dict - from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QWidget -from src.models.delivery_man.delivery_man import DeliveryMan from src.models.tour import ComputedDelivery, Delivery, Tour +from src.services.command.command_service import CommandService +from src.services.command.commands.update_delivery_request_time_window_command import ( + UpdateDeliveryRequestTimeWindow, +) from src.services.delivery_man.delivery_man_service import DeliveryManService -from src.services.tour.tour_service import TourService class ToursTableColumnItemTime(QWidget): @@ -71,8 +71,10 @@ def __handle_time_change(self, time_window: int): if time_window is None or time_window == -1: return - TourService.instance().update_delivery_request_time_window( - delivery_request_id=self.__delivery.id, - tour_id=self.__tour.id, - time_window=time_window, + CommandService.instance().execute( + UpdateDeliveryRequestTimeWindow( + delivery_request_id=self.__delivery.id, + tour_id=self.__tour.id, + time_window=time_window, + ) ) From 33d0c6f8a19e8c3e2fe3a35997973c7070850026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:18:34 +0100 Subject: [PATCH 07/13] rename command --- .../commands/update_delivery_request_time_window_command.py | 2 +- .../tours_table_column_items/tours_table_column_item_time.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/command/commands/update_delivery_request_time_window_command.py b/src/services/command/commands/update_delivery_request_time_window_command.py index 4724a0c..5a0c61f 100644 --- a/src/services/command/commands/update_delivery_request_time_window_command.py +++ b/src/services/command/commands/update_delivery_request_time_window_command.py @@ -5,7 +5,7 @@ from src.services.tour.tour_service import TourService -class UpdateDeliveryRequestTimeWindow(AbstractCommand): +class UpdateDeliveryRequestTimeWindowCommand(AbstractCommand): __delivery_request_id: DeliveryID __tour_id: TourID __time_window: int diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py index 19c5a82..792a830 100644 --- a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_time.py @@ -3,7 +3,7 @@ from src.models.tour import ComputedDelivery, Delivery, Tour from src.services.command.command_service import CommandService from src.services.command.commands.update_delivery_request_time_window_command import ( - UpdateDeliveryRequestTimeWindow, + UpdateDeliveryRequestTimeWindowCommand, ) from src.services.delivery_man.delivery_man_service import DeliveryManService @@ -72,7 +72,7 @@ def __handle_time_change(self, time_window: int): return CommandService.instance().execute( - UpdateDeliveryRequestTimeWindow( + UpdateDeliveryRequestTimeWindowCommand( delivery_request_id=self.__delivery.id, tour_id=self.__tour.id, time_window=time_window, From 424a4ea21b3a2dba1b676298fbcf0a58be45e283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:25:56 +0100 Subject: [PATCH 08/13] convert update delivery man on request as a command --- ...e_delivery_request_delivery_man_command.py | 42 +++++++++++++++++++ src/services/tour/tour_service.py | 6 ++- .../tours_table_column_item_delivery_man.py | 14 +++++-- 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 src/services/command/commands/update_delivery_request_delivery_man_command.py diff --git a/src/services/command/commands/update_delivery_request_delivery_man_command.py b/src/services/command/commands/update_delivery_request_delivery_man_command.py new file mode 100644 index 0000000..bbb9ef1 --- /dev/null +++ b/src/services/command/commands/update_delivery_request_delivery_man_command.py @@ -0,0 +1,42 @@ +from typing import Optional +from uuid import UUID + +from src.models.tour import DeliveryID, TourID +from src.services.command.abstract_command import AbstractCommand +from src.services.tour.tour_service import TourService + + +class UpdateDeliveryRequestDeliveryMan(AbstractCommand): + __delivery_request_id: DeliveryID + __tour_id: TourID + __delivery_man_id: UUID + __previous_delivery_man_id: Optional[UUID] = None + + def __init__( + self, delivery_request_id: DeliveryID, tour_id: TourID, delivery_man_id: UUID + ) -> None: + super().__init__("Ajustement du livreur d'une demande de livraison") + self.__delivery_request_id = delivery_request_id + self.__tour_id = tour_id + self.__delivery_man_id = delivery_man_id + + def execute(self) -> None: + self.__previous_delivery_man_id = ( + TourService.instance().update_delivery_request_delivery_man( + delivery_request_id=self.__delivery_request_id, + tour_id=self.__tour_id, + delivery_man_id=self.__delivery_man_id, + ) + ) + + def undo(self) -> None: + if self.__previous_delivery_man_id is None: + raise Exception("Cannot undo a command that has not been executed") + + TourService.instance().update_delivery_request_delivery_man( + delivery_request_id=self.__delivery_request_id, + tour_id=self.__delivery_man_id, + delivery_man_id=self.__previous_delivery_man_id, + ) + + self.__previous_delivery_man_id = None diff --git a/src/services/tour/tour_service.py b/src/services/tour/tour_service.py index 311ed1e..bc57b7f 100644 --- a/src/services/tour/tour_service.py +++ b/src/services/tour/tour_service.py @@ -169,13 +169,15 @@ def update_delivery_request_time_window( def update_delivery_request_delivery_man( self, delivery_request_id: DeliveryID, tour_id: TourID, delivery_man_id: UUID - ) -> None: + ) -> UUID: if tour_id == delivery_man_id: return tour_request = self.__tour_requests.value[tour_id] delivery_request = tour_request.deliveries[delivery_request_id] + previous_delivery_man_id = tour_request.delivery_man.id + del tour_request.deliveries[delivery_request_id] self.__get_or_create_tour_request(delivery_man_id).deliveries[ @@ -184,6 +186,8 @@ def update_delivery_request_delivery_man( self.__tour_requests.on_next(self.__tour_requests.value) + return previous_delivery_man_id + def compute_tours(self) -> None: """Compute the tours and publish the update.""" if len(self.__tour_requests.value) == 0: diff --git a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py index 3de9986..11d2532 100644 --- a/src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py +++ b/src/views/main_page/form/tours_table_column_items/tours_table_column_item_delivery_man.py @@ -5,6 +5,10 @@ from src.models.delivery_man.delivery_man import DeliveryMan from src.models.tour import Delivery, Tour +from src.services.command.command_service import CommandService +from src.services.command.commands.update_delivery_request_delivery_man_command import ( + UpdateDeliveryRequestDeliveryMan, +) from src.services.delivery_man.delivery_man_service import DeliveryManService from src.services.tour.tour_service import TourService @@ -68,8 +72,10 @@ def __handle_delivery_man_change(self, delivery_man_id: UUID): if delivery_man_id is None or delivery_man_id == self.__tour.delivery_man.id: return - TourService.instance().update_delivery_request_delivery_man( - delivery_request_id=self.__delivery.id, - tour_id=self.__tour.id, - delivery_man_id=delivery_man_id, + CommandService.instance().execute( + UpdateDeliveryRequestDeliveryMan( + delivery_request_id=self.__delivery.id, + tour_id=self.__tour.id, + delivery_man_id=delivery_man_id, + ) ) From 32302436f343b46361649abd90ee59ff00d5f414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:29:52 +0100 Subject: [PATCH 09/13] failsafe --- .../commands/update_delivery_request_time_window_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/command/commands/update_delivery_request_time_window_command.py b/src/services/command/commands/update_delivery_request_time_window_command.py index 5a0c61f..b9197bf 100644 --- a/src/services/command/commands/update_delivery_request_time_window_command.py +++ b/src/services/command/commands/update_delivery_request_time_window_command.py @@ -29,7 +29,7 @@ def execute(self) -> None: ) def undo(self) -> None: - if self.__time_window is None: + if self.__previous_time_window is None: raise Exception("Cannot undo a command that has not been executed") TourService.instance().update_delivery_request_time_window( @@ -37,3 +37,5 @@ def undo(self) -> None: tour_id=self.__tour_id, time_window=self.__previous_time_window, ) + + self.__previous_time_window = None From 97474b7c922985e65cc42a7e4d8998bfbe6b35cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:52:26 +0100 Subject: [PATCH 10/13] put back select delivery functionality --- src/services/tour/tour_service.py | 19 +++++++------- src/views/main_page/form/tours_table.py | 34 +++++++++++++++---------- src/views/main_page/map/map_view.py | 10 ++++---- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/services/tour/tour_service.py b/src/services/tour/tour_service.py index bc57b7f..ac1b63c 100644 --- a/src/services/tour/tour_service.py +++ b/src/services/tour/tour_service.py @@ -14,6 +14,7 @@ Tour, TourID, TourRequest, + Delivery ) from src.services.delivery_man.delivery_man_service import DeliveryManService from src.services.map.delivery_location_service import DeliveryLocationService @@ -29,12 +30,12 @@ class TourService(Singleton): __tour_requests: BehaviorSubject[Dict[TourID, TourRequest]] __computed_tours: BehaviorSubject[Dict[TourID, ComputedTour]] - __selected_delivery_request: BehaviorSubject[Optional[DeliveryRequest]] + __selected_delivery: BehaviorSubject[Optional[Delivery]] def __init__(self) -> None: self.__tour_requests = BehaviorSubject({}) self.__computed_tours = BehaviorSubject({}) - self.__selected_delivery_request = BehaviorSubject(None) + self.__selected_delivery = BehaviorSubject(None) self.__tour_requests.subscribe(lambda _: self.compute_tours()) @@ -45,9 +46,9 @@ def tour_requests(self) -> Observable[Dict[TourID, TourRequest]]: @property def tour_requests_delivery_locations( self, - ) -> Observable[Tuple[DeliveryLocation, List[DeliveryLocation]]]: + ) -> Observable[Tuple[Delivery, List[DeliveryLocation]]]: return combine_latest( - self.__selected_delivery_request, + self.__selected_delivery, self.__tour_requests, ).pipe( map( @@ -92,10 +93,10 @@ def get_tour_requests(self) -> List[TourRequest]: def get_computed_tours(self) -> List[ComputedTour]: return self.__computed_tours.value - def select_delivery_request( - self, delivery_request: Optional[DeliveryRequest] + def select_delivery( + self, delivery: Optional[Delivery] ) -> None: - self.__selected_delivery_request.on_next(delivery_request) + self.__selected_delivery.on_next(delivery) def add_delivery_request( self, position: Position, time_window: int, tour_id: TourID @@ -146,8 +147,8 @@ def remove_delivery_request( self.__tour_requests.on_next(self.__tour_requests.value) - if self.__selected_delivery_request.value == tour_request: - self.__selected_delivery_request.on_next(None) + if self.__selected_delivery.value == tour_request: + self.__selected_delivery.on_next(None) return delivery_request diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py index 2c27226..adb4daa 100644 --- a/src/views/main_page/form/tours_table.py +++ b/src/views/main_page/form/tours_table.py @@ -1,8 +1,9 @@ -from typing import Dict, List +from typing import List, Tuple -from PyQt6.QtWidgets import QLabel, QTableWidget +from PyQt6.QtWidgets import QTableWidget +from src.services.tour.tour_service import TourService -from src.models.tour import Tour +from src.models.tour import Tour, Delivery from src.views.main_page.form.tours_table_column import ToursTableColumn from src.views.main_page.form.tours_table_column_items import ( ToursTableColumnItemActions, @@ -36,24 +37,28 @@ class ToursTable(QTableWidget): ), ToursTableColumn(header="", render=ToursTableColumnItemActions, width=80), ] + __values: List[Tuple[Tour, Delivery]] = [] def __init__(self): super().__init__() self.__setup_table() + + self.cellClicked.connect(lambda: self.__on_item_clicked()) def update_content(self, tours: List[Tour]) -> None: self.setRowCount(0) + + self.__values = [(tour, delivery) for tour in tours for delivery in tour.deliveries.values()] + + for tour, delivery in self.__values: + row = self.rowCount() - for tour in tours: - for delivery in tour.deliveries.values(): - row = self.rowCount() - - self.insertRow(row) - - for column, column_factory in enumerate(self.COLUMNS): - self.setCellWidget( - row, column, column_factory.render(tour, delivery) - ) + self.insertRow(row) + + for column, column_factory in enumerate(self.COLUMNS): + self.setCellWidget( + row, column, column_factory.render(tour, delivery) + ) def __setup_table(self): self.setColumnCount(len(self.COLUMNS)) @@ -64,3 +69,6 @@ def __setup_table(self): for i, col in enumerate(self.COLUMNS): if col.width is not None: self.setColumnWidth(i, col.width) + + def __on_item_clicked(self) -> None: + TourService.instance().select_delivery(self.__values[self.currentRow()][1]) \ No newline at end of file diff --git a/src/views/main_page/map/map_view.py b/src/views/main_page/map/map_view.py index d6968e4..05d1d0e 100644 --- a/src/views/main_page/map/map_view.py +++ b/src/views/main_page/map/map_view.py @@ -25,7 +25,7 @@ from reactivex.subject import BehaviorSubject from src.models.map import Map, Position, Segment -from src.models.tour import ComputedTour, DeliveryLocation, TourID +from src.models.tour import ComputedTour, DeliveryLocation, TourID, Delivery from src.services.command.command_service import CommandService from src.services.command.commands.add_delivery_request_command import ( AddDeliveryRequestCommand, @@ -234,10 +234,10 @@ def mouseDoubleClickEvent(self, event: QMouseEvent | None) -> None: def __on_update_delivery_locations( self, - deliveries: Tuple[Optional[DeliveryLocation], List[DeliveryLocation]], + deliveries: Tuple[Optional[Delivery], List[DeliveryLocation]], ): - selected_delivery_location, delivery_locations = deliveries - + selected_delivery, delivery_locations = deliveries + for marker in self.__map_annotations.markers.get(MarkersTypes.Delivery): self.__scene.removeItem(marker.shape) @@ -250,7 +250,7 @@ def __on_update_delivery_locations( position=delivery_location.segment.origin, icon="map-marker-alt", color=QColor("#f54242"), - scale=1.5 if delivery_location == selected_delivery_location else 1, + scale=1.5 if (selected_delivery and delivery_location == selected_delivery.location) else 1, ), ) From f43651d71822919a33480b072cc053af6556304c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:55:50 +0100 Subject: [PATCH 11/13] fix color bug --- src/views/main_page/map/map_view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/main_page/map/map_view.py b/src/views/main_page/map/map_view.py index 05d1d0e..b3db9f1 100644 --- a/src/views/main_page/map/map_view.py +++ b/src/views/main_page/map/map_view.py @@ -466,6 +466,8 @@ def __set_brush_for_segment( for i in range(count): brush.setColorAt(i / count, colors[i % len(colors)]) brush.setColorAt(i / count + 0.0000001, colors[(i + 1) % len(colors)]) + + brush.setColorAt(1, colors[count % len(colors)]) segment_pen = segment_shape.pen() segment_pen.setBrush(brush) From 71e15c43e1d14e79363b2797a4bc011785df9829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 17:59:03 +0100 Subject: [PATCH 12/13] lint --- ...te_delivery_request_time_window_command.py | 2 +- src/services/tour/tour_service.py | 6 ++--- src/views/main_page/form/tours_table.py | 22 +++++++++---------- src/views/main_page/map/map_view.py | 13 +++++++---- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/services/command/commands/update_delivery_request_time_window_command.py b/src/services/command/commands/update_delivery_request_time_window_command.py index b9197bf..c805c02 100644 --- a/src/services/command/commands/update_delivery_request_time_window_command.py +++ b/src/services/command/commands/update_delivery_request_time_window_command.py @@ -37,5 +37,5 @@ def undo(self) -> None: tour_id=self.__tour_id, time_window=self.__previous_time_window, ) - + self.__previous_time_window = None diff --git a/src/services/tour/tour_service.py b/src/services/tour/tour_service.py index ac1b63c..a5be19c 100644 --- a/src/services/tour/tour_service.py +++ b/src/services/tour/tour_service.py @@ -8,13 +8,13 @@ from src.models.map import Position from src.models.tour import ( ComputedTour, + Delivery, DeliveryID, DeliveryLocation, DeliveryRequest, Tour, TourID, TourRequest, - Delivery ) from src.services.delivery_man.delivery_man_service import DeliveryManService from src.services.map.delivery_location_service import DeliveryLocationService @@ -93,9 +93,7 @@ def get_tour_requests(self) -> List[TourRequest]: def get_computed_tours(self) -> List[ComputedTour]: return self.__computed_tours.value - def select_delivery( - self, delivery: Optional[Delivery] - ) -> None: + def select_delivery(self, delivery: Optional[Delivery]) -> None: self.__selected_delivery.on_next(delivery) def add_delivery_request( diff --git a/src/views/main_page/form/tours_table.py b/src/views/main_page/form/tours_table.py index adb4daa..b7727a4 100644 --- a/src/views/main_page/form/tours_table.py +++ b/src/views/main_page/form/tours_table.py @@ -1,9 +1,9 @@ from typing import List, Tuple from PyQt6.QtWidgets import QTableWidget -from src.services.tour.tour_service import TourService -from src.models.tour import Tour, Delivery +from src.models.tour import Delivery, Tour +from src.services.tour.tour_service import TourService from src.views.main_page.form.tours_table_column import ToursTableColumn from src.views.main_page.form.tours_table_column_items import ( ToursTableColumnItemActions, @@ -42,23 +42,23 @@ class ToursTable(QTableWidget): def __init__(self): super().__init__() self.__setup_table() - + self.cellClicked.connect(lambda: self.__on_item_clicked()) def update_content(self, tours: List[Tour]) -> None: self.setRowCount(0) - - self.__values = [(tour, delivery) for tour in tours for delivery in tour.deliveries.values()] - + + self.__values = [ + (tour, delivery) for tour in tours for delivery in tour.deliveries.values() + ] + for tour, delivery in self.__values: row = self.rowCount() self.insertRow(row) - + for column, column_factory in enumerate(self.COLUMNS): - self.setCellWidget( - row, column, column_factory.render(tour, delivery) - ) + self.setCellWidget(row, column, column_factory.render(tour, delivery)) def __setup_table(self): self.setColumnCount(len(self.COLUMNS)) @@ -71,4 +71,4 @@ def __setup_table(self): self.setColumnWidth(i, col.width) def __on_item_clicked(self) -> None: - TourService.instance().select_delivery(self.__values[self.currentRow()][1]) \ No newline at end of file + TourService.instance().select_delivery(self.__values[self.currentRow()][1]) diff --git a/src/views/main_page/map/map_view.py b/src/views/main_page/map/map_view.py index b3db9f1..6493bfc 100644 --- a/src/views/main_page/map/map_view.py +++ b/src/views/main_page/map/map_view.py @@ -25,7 +25,7 @@ from reactivex.subject import BehaviorSubject from src.models.map import Map, Position, Segment -from src.models.tour import ComputedTour, DeliveryLocation, TourID, Delivery +from src.models.tour import ComputedTour, Delivery, DeliveryLocation, TourID from src.services.command.command_service import CommandService from src.services.command.commands.add_delivery_request_command import ( AddDeliveryRequestCommand, @@ -237,7 +237,7 @@ def __on_update_delivery_locations( deliveries: Tuple[Optional[Delivery], List[DeliveryLocation]], ): selected_delivery, delivery_locations = deliveries - + for marker in self.__map_annotations.markers.get(MarkersTypes.Delivery): self.__scene.removeItem(marker.shape) @@ -250,7 +250,12 @@ def __on_update_delivery_locations( position=delivery_location.segment.origin, icon="map-marker-alt", color=QColor("#f54242"), - scale=1.5 if (selected_delivery and delivery_location == selected_delivery.location) else 1, + scale=1.5 + if ( + selected_delivery + and delivery_location == selected_delivery.location + ) + else 1, ), ) @@ -466,7 +471,7 @@ def __set_brush_for_segment( for i in range(count): brush.setColorAt(i / count, colors[i % len(colors)]) brush.setColorAt(i / count + 0.0000001, colors[(i + 1) % len(colors)]) - + brush.setColorAt(1, colors[count % len(colors)]) segment_pen = segment_shape.pen() From 15c81af518e2582e3acd0eb3c143df67e81ba62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Fran=C3=A7ois=20St-Cyr?= Date: Tue, 14 Nov 2023 18:16:16 +0100 Subject: [PATCH 13/13] add coverage --- .vscode/settings.json | 2 +- Pipfile | 1 + Pipfile.lock | 104 +++++++++++++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6779a6b..702c211 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "python.testing.pytestArgs": [ - "." + "--cov-report=xml", "--cov-report=term", "--cov=src", "src" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, diff --git a/Pipfile b/Pipfile index 107d4a8..e0fdc3c 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ black = "*" isort = "*" pytest = "*" pyinstaller = "*" +pytest-cov = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 27cc085..6d16d53 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0cc8382949101e2c5a49a54761487d90fb1166dbf763d5d17a9e5e4e4618592a" + "sha256": "01fc4795118b197464d4ca89f49b2f83a4a4f106929583ea467d83d4a2be722e" }, "pipfile-spec": 6, "requires": { @@ -156,6 +156,67 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1", + "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63", + "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9", + "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312", + "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3", + "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb", + "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25", + "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92", + "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda", + "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148", + "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6", + "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216", + "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a", + "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640", + "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836", + "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c", + "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f", + "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2", + "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901", + "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed", + "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a", + "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074", + "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc", + "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84", + "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083", + "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f", + "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c", + "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c", + "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637", + "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2", + "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82", + "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f", + "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce", + "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef", + "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f", + "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611", + "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c", + "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76", + "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9", + "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce", + "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9", + "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf", + "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf", + "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9", + "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6", + "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2", + "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a", + "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a", + "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf", + "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738", + "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a", + "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4" + ], + "markers": "python_version >= '3.8'", + "version": "==7.3.2" + }, "exceptiongroup": { "hashes": [ "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", @@ -215,11 +276,11 @@ }, "platformdirs": { "hashes": [ - "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", - "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" + "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", + "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" ], "markers": "python_version >= '3.7'", - "version": "==3.11.0" + "version": "==4.0.0" }, "pluggy": { "hashes": [ @@ -231,22 +292,22 @@ }, "pyinstaller": { "hashes": [ - "sha256:041ab9311d08162356829bf47293a613c44dc9ace28846fb63098889c7383c5d", - "sha256:331f050e8f9e923bb6b50454acfc0547fd52092585c61eb5f2fc93de60703f13", - "sha256:3c04963637481a3edf1eec64ab4c3fce098908f02fc472c11e73be7eedc08b95", - "sha256:4368e4eb9999ce32e3280330b3c26f175e0fa7fa13efb4d2dc4ade488ff6d7c2", - "sha256:6e71d9f6f5a1e0f7523e8ebee1b76bb29538f64d863e3711c2b21033f499e2b9", - "sha256:8f3d49c60f3344bf3d4a6d4258bda665dad185ab2b097341d3af2a6387c838ef", - "sha256:9e8b5bbc1bdf554ade1360e62e4959091430c3cc15ebfff3c28c8894fd1f312a", - "sha256:bebf6f442bbe6343acaec873803510ee1930d026846a018f727da4e0690081f8", - "sha256:da78942d31c1911ea4abcd3ca3bd0c062af7f163a5e227fd18a359b61deda4ca", - "sha256:dd438afd2abb643f5399c0cb254a11c217c06782cb274a2911dd785f9f67fa9e", - "sha256:f63d2353537bac7bfeeaedbe5ac99f3be35daa290dd1ad1be90768acbf77e3d5", - "sha256:f9f5bcaef6122d93c54ee7a9ecb07eab5b81a7ebfb5cb99af2b2a6ff49eff62f" + "sha256:104430686149b2f1c135b2c17aa2967c85d54ef77dc92feb4e179ec846c0c467", + "sha256:1ce77043929bf525be38289d78feecde0fcf15506215eda6500176a8715c5047", + "sha256:29d164394f1e949072f78a64c1e040f1c47b7f4aff08514c7666a031c8b44996", + "sha256:3b586196277c4c54b69880650984c39c28bb6258c2b4b64200032e6ac69d53a0", + "sha256:8ec9d6c98972bb922cedb16a6638257aa66e5deadd79e2953f3464696237c413", + "sha256:a1adbd3cf25dc90926d783eae0f444d65cdfecc7bcdf6da522c3ae3ff47b4c25", + "sha256:ba602a38d7403de89c38b8956b221ce6de0280730d269bab522492fcad82ee33", + "sha256:d0c87b605bf13c3a04dfaa1d2fa7cd36765b8137000eeadccba865e1d6a19bf0", + "sha256:e5561e9a9b946d835c8dbc11ae4c16cc21e62bc77d10cc043406dc2992dfb4c6", + "sha256:e87fd60292b53bb9965cb5a84122875469a2bd475fd0d0db0052a3f1be351f75", + "sha256:ebac06d99b80d2035594c3cc2fb5f2612d86289edd0510dbcbeb20a873f51d5a", + "sha256:fcfabc0ff1d38a4262c051dea3fdc1f7f106405c1f1b491b4c79cd28df19cab6" ], "index": "pypi", "markers": "python_version < '3.13' and python_version >= '3.8'", - "version": "==6.1.0" + "version": "==6.2.0" }, "pyinstaller-hooks-contrib": { "hashes": [ @@ -265,6 +326,15 @@ "markers": "python_version >= '3.7'", "version": "==7.4.3" }, + "pytest-cov": { + "hashes": [ + "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", + "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.1.0" + }, "setuptools": { "hashes": [ "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",