From 48a8588358dc39e9c76bc8a7b060c2754e26c9d0 Mon Sep 17 00:00:00 2001 From: Michele-Alberti <62114934+Michele-Alberti@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:43:04 +0200 Subject: [PATCH] feat: add button to change lunch time or takeaway option add change_order_time_takeaway_button to GraphicInterface and add it to buttons_flexbox add change_order_time_takeaway_button to reload_on_no_more_order_callback to change its enabled state change toggle_no_more_order_button icon add change_order_time_takeaway to core.py add try-except statement to delete_order to handle database errors (as done in send_order) update reload_menu to handle change_order_time_takeaway_button visibility move lunch_time column from 'orders' table to 'users' table and add back_populates to both tables change queries for postgresql and sqlite to join lunch time to orders_query result remove confirm_message from GraphicInterface remove unused parameters from functions in core.py and related callbacks --- dlunch/__init__.py | 1 - dlunch/conf/db/postgresql.yaml | 6 +- dlunch/conf/db/sqlite.yaml | 6 +- dlunch/core.py | 147 +++++++++++++++++++++++++++------ dlunch/gui.py | 32 +++++-- dlunch/models.py | 9 +- 6 files changed, 159 insertions(+), 42 deletions(-) diff --git a/dlunch/__init__.py b/dlunch/__init__.py index c2118c4..32d7747 100755 --- a/dlunch/__init__.py +++ b/dlunch/__init__.py @@ -102,7 +102,6 @@ def create_app(config: DictConfig) -> pn.Template: app.main.append(gi.results_divider) app.main.append(gi.res_col) app.modal.append(gi.error_message) - app.modal.append(gi.confirm_message) # Set components visibility based on no_more_order_button state # and reload menu diff --git a/dlunch/conf/db/postgresql.yaml b/dlunch/conf/db/postgresql.yaml index a2bd9f4..87bdd52 100644 --- a/dlunch/conf/db/postgresql.yaml +++ b/dlunch/conf/db/postgresql.yaml @@ -14,10 +14,12 @@ url: ${db.dialect}+${db.driver}://${db.username}:${db.password}@${db.host}:${db. # QUERIES # Orders orders_query: |- - SELECT o.user, o.lunch_time, m.item, o.note + SELECT o.user, u.lunch_time, m.item, o.note FROM {schema}.orders o LEFT JOIN {schema}.menu m - ON m.id = o.menu_item_id; + ON m.id = o.menu_item_id + LEFT JOIN {schema}.users u + ON u.id = o.user; # Stats stats_query: |- SELECT EXTRACT(YEAR FROM date)::varchar(4) AS "Year", diff --git a/dlunch/conf/db/sqlite.yaml b/dlunch/conf/db/sqlite.yaml index 606f192..e4a597f 100644 --- a/dlunch/conf/db/sqlite.yaml +++ b/dlunch/conf/db/sqlite.yaml @@ -9,10 +9,12 @@ url: ${db.dialect}:///${db.db_path} # QUERIES # Orders orders_query: |- - SELECT o.user, o.lunch_time, m.item, o.note + SELECT o.user, u.lunch_time, m.item, o.note FROM orders o LEFT JOIN menu m - ON m.id = o.menu_item_id; + ON m.id = o.menu_item_id + LEFT JOIN users u + ON u.id = o.user; # Stats stats_query: |- SELECT STRFTIME('%Y', date) AS "Year", diff --git a/dlunch/core.py b/dlunch/core.py index be69363..2db08ab 100644 --- a/dlunch/core.py +++ b/dlunch/core.py @@ -15,7 +15,7 @@ from pytesseract import pytesseract import random import re -from sqlalchemy import func, select, delete +from sqlalchemy import func, select, delete, update from sqlalchemy.sql.expression import true as sql_true from time import sleep @@ -166,7 +166,6 @@ def build_menu( ) -> pd.DataFrame: # Hide messages gi.error_message.visible = False - gi.confirm_message.visible = False # Build image path menu_filename = str( @@ -316,17 +315,24 @@ def reload_menu( value_if_missing=False, ) - # Set no more orders toggle button visibility and activation + # Set no more orders toggle button and the change order time button + # visibility and activation if auth.is_guest( user=pn_user(config), config=config, allow_override=False ): - # Deactivate the no_more_orders button for guest users + # Deactivate the no_more_orders_button for guest users gi.toggle_no_more_order_button.disabled = True gi.toggle_no_more_order_button.visible = False + # Deactivate the change_order_time_button for guest users + gi.change_order_time_takeaway_button.disabled = True + gi.change_order_time_takeaway_button.visible = False else: - # Activate the no_more_orders button for privileged users + # Activate the no_more_orders_button for privileged users gi.toggle_no_more_order_button.disabled = False gi.toggle_no_more_order_button.visible = True + # Show the change_order_time_button for privileged users + # It is disabled by the no more order button if necessary + gi.change_order_time_takeaway_button.visible = True # Guest graphic configuration if auth.is_guest(user=pn_user(config), config=config): @@ -586,7 +592,6 @@ def send_order( # Hide messages gi.error_message.visible = False - gi.confirm_message.visible = False # Create session session = models.create_session(config) @@ -677,11 +682,13 @@ def send_order( new_user = models.Users( id=username_key_press, guest=person.guest, + lunch_time=person.lunch_time, takeaway=person.takeaway, ) else: new_user = models.Users( id=username_key_press, + lunch_time=person.lunch_time, takeaway=person.takeaway, ) session.add(new_user) @@ -691,7 +698,6 @@ def send_order( # Order new_order = models.Orders( user=username_key_press, - lunch_time=person.lunch_time, menu_item_id=row.Index, note=getattr( row, config.panel.gui.note_column_name @@ -722,7 +728,7 @@ def send_order( f"DATABASE ERROR

ERROR:
{str(e)}" ) gi.error_message.visible = True - log.warning("database error") + log.error("database error") # Open modal window app.open_modal() else: @@ -744,7 +750,6 @@ def delete_order( event, config: DictConfig, app: pn.Template, - person: gui.Person, gi: gui.GraphicInterface, ) -> None: # Get username, updated on every keypress @@ -752,7 +757,6 @@ def delete_order( # Hide messages gi.error_message.visible = False - gi.confirm_message.visible = False # Create session session = models.create_session(config) @@ -797,21 +801,114 @@ def delete_order( return # Delete user - num_rows_deleted_users = session.execute( - delete(models.Users).where( - models.Users.id == username_key_press + try: + num_rows_deleted_users = session.execute( + delete(models.Users).where( + models.Users.id == username_key_press + ) ) - ) - # Delete also orders (hotfix for Debian) - num_rows_deleted_orders = session.execute( - delete(models.Orders).where( - models.Orders.user == username_key_press + # Delete also orders (hotfix for Debian) + num_rows_deleted_orders = session.execute( + delete(models.Orders).where( + models.Orders.user == username_key_press + ) ) + session.commit() + if (num_rows_deleted_users.rowcount > 0) or ( + num_rows_deleted_orders.rowcount > 0 + ): + # Update dataframe widget + reload_menu( + None, + config, + gi, + ) + + pn.state.notifications.success( + "Order canceled", + duration=config.panel.notifications.duration, + ) + log.info(f"{username_key_press}'s order canceled") + else: + pn.state.notifications.warning( + f'No order for user named
"{username_key_press}"', + duration=config.panel.notifications.duration, + ) + log.info(f"no order for user named {username_key_press}") + except Exception as e: + # Any exception here is a database fault + pn.state.notifications.error( + "Database error", + duration=config.panel.notifications.duration, + ) + gi.error_message.object = ( + f"DATABASE ERROR

ERROR:
{str(e)}" + ) + gi.error_message.visible = True + log.error("database error") + # Open modal window + app.open_modal() + else: + pn.state.notifications.warning( + "Please insert user name", + duration=config.panel.notifications.duration, + ) + log.warning("missing username") + + +def change_order_time_takeaway( + event, + config: DictConfig, + person: gui.Person, + gi: gui.GraphicInterface, +) -> None: + # Get username, updated on every keypress + username_key_press = gi.person_widget._widgets["username"].value_input + + # Create session + session = models.create_session(config) + + with session: + # Check if the "no more order" toggle button is pressed + if models.get_flag(config=config, id="no_more_orders"): + pn.state.notifications.error( + "It is not possible to update orders (time)", + duration=config.panel.notifications.duration, ) + + # Reload the menu + reload_menu( + None, + config, + gi, + ) + + return + + if username_key_press: + # Build and execute the update statement + update_statement = ( + update(models.Users) + .where(models.Users.id == username_key_press) + .values(lunch_time=person.lunch_time, takeaway=person.takeaway) + .returning(models.Users) + ) + + updated_user = session.scalars(update_statement).one_or_none() + session.commit() - if (num_rows_deleted_users.rowcount > 0) or ( - num_rows_deleted_orders.rowcount > 0 - ): + + if updated_user: + # Find updated values + updated_time = updated_user.lunch_time + updated_takeaway = ( + (" " + config.panel.gui.takeaway_id) + if updated_user.takeaway + else "" + ) + updated_items_names = [ + order.menu_item.item for order in updated_user.orders + ] # Update dataframe widget reload_menu( None, @@ -820,10 +917,10 @@ def delete_order( ) pn.state.notifications.success( - "Order canceled", + f"{username_key_press}'s
lunch time changed to
{updated_time}{updated_takeaway}
({', '.join(updated_items_names)})", duration=config.panel.notifications.duration, ) - log.info(f"{username_key_press}'s order canceled") + log.info(f"{username_key_press}'s order updated") else: pn.state.notifications.warning( f'No order for user named
"{username_key_press}"', @@ -952,12 +1049,8 @@ def clean_up_table( def download_dataframe( config: DictConfig, - app: pn.Template, gi: gui.GraphicInterface, ) -> None: - # Hide messages - gi.error_message.visible = False - gi.confirm_message.visible = False # Build a dict of dataframes, one for each lunch time (the key contains # a lunch time) diff --git a/dlunch/gui.py b/dlunch/gui.py index cb111fd..4be808f 100644 --- a/dlunch/gui.py +++ b/dlunch/gui.py @@ -383,7 +383,17 @@ def reload_on_guest_override_callback( button_style="outline", button_type="warning", height=generic_button_height, - icon="alarm", + icon="hand-stop", + icon_size="2em", + sizing_mode="stretch_width", + ) + # Create change time + self.change_order_time_takeaway_button = pnw.Button( + name="Change Time/Takeaway", + button_type="primary", + button_style="outline", + height=generic_button_height, + icon="clock-edit", icon_size="2em", sizing_mode="stretch_width", ) @@ -432,6 +442,7 @@ def reload_on_guest_override_callback( *[ self.send_order_button, self.toggle_no_more_order_button, + self.change_order_time_takeaway_button, self.delete_order_button, ], flex_wrap="nowrap", @@ -454,9 +465,10 @@ def reload_on_no_more_order_callback( # Show "no more order" text self.no_more_order_alert.visible = toggle - # Deactivate send order and delete order buttons + # Deactivate send, delete and change order buttons self.send_order_button.disabled = toggle self.delete_order_button.disabled = toggle + self.change_order_time_takeaway_button.disabled = toggle # Simply reload the menu when the toggle button value changes if reload: @@ -493,6 +505,14 @@ def reload_on_no_more_order_callback( e, config, app, + self, + ) + ) + # Change order time button callback + self.change_order_time_takeaway_button.on_click( + lambda e: core.change_order_time_takeaway( + e, + config, person, self, ) @@ -505,12 +525,6 @@ def reload_on_no_more_order_callback( sizing_mode="stretch_width", ) self.error_message.visible = False - # Confirm message - self.confirm_message = pn.pane.HTML( - styles={"color": "green", "font-weight": "bold"}, - sizing_mode="stretch_width", - ) - self.confirm_message.visible = False # SIDEBAR ------------------------------------------------------------- # TEXTS @@ -610,7 +624,7 @@ def reload_on_no_more_order_callback( ) # Download button and callback self.download_button = pn.widgets.FileDownload( - callback=lambda: core.download_dataframe(config, app, self), + callback=lambda: core.download_dataframe(config, self), filename=config.panel.file_name + ".xlsx", sizing_mode="stretch_width", icon="download", diff --git a/dlunch/models.py b/dlunch/models.py index 9be9be3..8b61979 100755 --- a/dlunch/models.py +++ b/dlunch/models.py @@ -212,7 +212,7 @@ class Orders(Data): index=True, nullable=False, ) - lunch_time = Column(String(7), index=True, nullable=False) + user_record = relationship("Users", back_populates="orders", uselist=False) menu_item_id = Column( Integer, ForeignKey("menu.id", ondelete="CASCADE"), @@ -265,9 +265,16 @@ class Users(Data): default="NotAGuest", server_default="NotAGuest", ) + lunch_time = Column(String(7), index=True, nullable=False) takeaway = Column( Boolean, nullable=False, default=False, server_default=sql_false() ) + orders = relationship( + "Orders", + back_populates="user_record", + cascade="all, delete-orphan", + passive_deletes=True, + ) @classmethod def clear(self, config: DictConfig) -> int: