From 3a9995d6513aae314ede6d50008f6f3fb2b4cf51 Mon Sep 17 00:00:00 2001 From: Vladimir Peter Date: Sat, 15 Apr 2023 14:53:16 +0800 Subject: [PATCH 1/5] fix: profile photo upload - Now getting path to file, then displaying the profile pic as a base64 string --- .gitignore | 1 + app.py | 20 ------------- tuttle/app/auth/view.py | 50 ++++++++++++++++----------------- tuttle/app/core/abstractions.py | 2 -- tuttle/app/core/utils.py | 15 ++++++++++ tuttle/app/timetracking/view.py | 47 ++++++++++++++++--------------- 6 files changed, 65 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index 671c56c4..2fd0f6d9 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,4 @@ app/assets/uploads # visual studio workspaces *.code-workspace +assets/uploads/* diff --git a/app.py b/app.py index 34495b3b..d0c79337 100644 --- a/app.py +++ b/app.py @@ -105,14 +105,12 @@ def page_resize(self, e): def pick_file_callback( self, on_file_picker_result, - on_upload_progress, allowed_extensions, dialog_title, file_type, ): # used by views to request a file upload self.file_picker.on_result = on_file_picker_result - self.file_picker.on_upload = on_upload_progress self.file_picker.pick_files( allow_multiple=False, allowed_extensions=allowed_extensions, @@ -120,23 +118,6 @@ def pick_file_callback( file_type=file_type, ) - def upload_file_callback(self, file): - try: - upload_to = self.page.get_upload_url(file.name, 600) - upload_item = FilePickerUploadFile( - file.name, - upload_url=upload_to, - ) - self.file_picker.upload([upload_item]) - - upload_path_in_assets = f"{get_assets_uploads_url()}/{file.name}" - return upload_path_in_assets - except Exception as e: - logger.error( - f"Exception @app.upload_file_callback raised during file upload {e.__class__.__name__}" - ) - logger.exception(e) - return None def on_theme_mode_changed(self, selected_theme: str): """callback function used by views for changing app theme mode""" @@ -281,7 +262,6 @@ def __init__(self, app: TuttleApp): dialog_controller=app.control_alert_dialog, on_navigate_back=app.on_view_pop, client_storage=app.client_storage, - upload_file_callback=app.upload_file_callback, pick_file_callback=app.pick_file_callback, ) diff --git a/tuttle/app/auth/view.py b/tuttle/app/auth/view.py index d8262010..94832956 100644 --- a/tuttle/app/auth/view.py +++ b/tuttle/app/auth/view.py @@ -1,5 +1,5 @@ from typing import Callable, Optional - +from pathlib import Path from flet import ( Column, Container, @@ -501,7 +501,6 @@ def on_update_photo_clicked(self, e): """Callback for when user clicks on the update photo button""" self.pick_file_callback( on_file_picker_result=self.on_profile_photo_picked, - on_upload_progress=self.uploading_profile_pic_progress_listener, allowed_extensions=["png", "jpeg", "jpg"], dialog_title="Tuttle profile photo", file_type="custom", @@ -511,30 +510,29 @@ def on_profile_photo_picked(self, e): """Callback for when profile photo has been picked""" if e.files and len(e.files) > 0: file = e.files[0] - upload_url = self.upload_file_callback(file) + upload_url = Path(file.path) if upload_url: - self.uploaded_photo_path = upload_url - - def uploading_profile_pic_progress_listener(self, e): - """Callback for when profile photo is being uploaded""" - if e.progress == 1.0: - if self.uploaded_photo_path: - result = self.intent.update_user_photo_path( - self.user_profile, - self.uploaded_photo_path, - ) - # assume error occurred - msg = result.error_msg - is_err = True - if result.was_intent_successful: - self.profile_photo_img.src = self.uploaded_photo_path - msg = "Profile photo updated" - is_err = False - self.show_snack(msg, is_err) - if is_err: - self.user_profile.profile_photo_path = "" - self.uploaded_photo_path = None # clear - self.update_self() + self.uploaded_photo_path = str(upload_url) + self.setProfilePic() + + def setProfilePic(self): + """Updates the profile photo""" + if not self.uploaded_photo_path: + return + result = self.intent.update_user_photo_path(self.user_profile, self.uploaded_photo_path,) + # assume error occurred + msg = result.error_msg + is_err = True + if result.was_intent_successful: + self.profile_photo_img.src_base64 = utils.toBase64(self.uploaded_photo_path) + msg = "Profile photo updated" + is_err = False + self.show_snack(msg, is_err) + if is_err: + self.user_profile.profile_photo_path = "" + self.uploaded_photo_path = None # clear + self.update_self() + def build(self): self.profile_photo_img = views.TProfilePhotoImg() @@ -564,7 +562,7 @@ def did_mount(self): else: self.user_profile: User = result.data if self.user_profile.profile_photo_path: - self.profile_photo_img.src = self.user_profile.profile_photo_path + self.profile_photo_img.src_base64 = utils.toBase64(self.user_profile.profile_photo_path) self.update_self() def will_unmount(self): diff --git a/tuttle/app/core/abstractions.py b/tuttle/app/core/abstractions.py index f5ac8b51..05af6ce3 100644 --- a/tuttle/app/core/abstractions.py +++ b/tuttle/app/core/abstractions.py @@ -95,7 +95,6 @@ class TViewParams: navigate_to_route: Callable show_snack: Callable dialog_controller: Callable - upload_file_callback: Callable pick_file_callback: Callable[[file_picker.FilePickerFile], str] client_storage: ClientStorage vertical_alignment_in_parent: str = START_ALIGNMENT @@ -118,7 +117,6 @@ def __init__(self, params: TViewParams): self.keep_back_stack = params.keep_back_stack self.navigate_back = params.on_navigate_back self.page_scroll_type = params.page_scroll_type - self.upload_file_callback = params.upload_file_callback self.pick_file_callback = params.pick_file_callback self.client_storage = params.client_storage self.mounted = False diff --git a/tuttle/app/core/utils.py b/tuttle/app/core/utils.py index f3e5f332..0177a408 100644 --- a/tuttle/app/core/utils.py +++ b/tuttle/app/core/utils.py @@ -1,4 +1,5 @@ import warnings +import base64 warnings.warn( "wastebasket module, content will be moved to other modules", @@ -144,3 +145,17 @@ def get_currencies() -> List[Tuple[str, str, str]]: # sort alphabetically by abbreviation currencies.sort(key=lambda tup: tup[1]) return currencies + + +def toBase64( + image_path, +) -> str: + """Returns base64 encoded image from the path""" + + # Read the image file as bytes + with open(image_path, "rb") as image_file: + image_bytes = image_file.read() + # Convert the bytes to a base64 + stringbase64_string = base64.b64encode(image_bytes).decode("utf-8") + + return stringbase64_string diff --git a/tuttle/app/timetracking/view.py b/tuttle/app/timetracking/view.py index 7d1c8fce..a4f93847 100644 --- a/tuttle/app/timetracking/view.py +++ b/tuttle/app/timetracking/view.py @@ -204,11 +204,12 @@ def on_add_timetrack_from_file( ): """Open file picker to select a file to upload""" self.close_pop_up_if_open() + if not is_spreadsheet and not is_ics: + return allowed_exts = ["ics"] if is_ics else ["xlsx", "csv", "xls", "tsv", "ods"] title = "Select .ics file" if is_ics else "Select excel file" self.pick_file_callback( on_file_picker_result=self.on_file_picker_result, - on_upload_progress=self.on_upload_progress, allowed_extensions=allowed_exts, dialog_title=title, file_type="custom", @@ -220,33 +221,35 @@ def on_file_picker_result(self, e: FilePickerResultEvent): if e.files and len(e.files) > 0: file = e.files[0] self.set_progress_hint(f"Uploading file {file.name}") - self.upload_file_callback(file) upload_path = Path(file.path) if upload_path: self.uploaded_file_path = upload_path + self.extract_dataframe_from_file() else: self.set_progress_hint(hide_progress=True) - def on_upload_progress(self, e: FilePickerUploadEvent): - """Handle file upload progress""" - if e.progress == 1.0: - # upload complete - self.set_progress_hint(f"Upload complete, processing file...") - intent_result = self.intent.process_timetracking_file( - self.uploaded_file_path, - ) - msg = ( - "New work progress recorded." - if intent_result.was_intent_successful - else intent_result.error_msg - ) - is_error = not intent_result.was_intent_successful - self.show_snack(msg, is_error) - if intent_result.was_intent_successful: - self.dataframe_to_display = intent_result.data - self.update_timetracking_dataframe() - self.display_dataframe() - self.set_progress_hint(hide_progress=True) + def extract_dataframe_from_file(self,): + + """Execute intent to process file""" + if not self.uploaded_file_path: + return + # upload complete + self.set_progress_hint(f"Upload complete, processing file...") + intent_result = self.intent.process_timetracking_file( + self.uploaded_file_path, + ) + msg = ( + "New work progress recorded." + if intent_result.was_intent_successful + else intent_result.error_msg + ) + is_error = not intent_result.was_intent_successful + self.show_snack(msg, is_error) + if intent_result.was_intent_successful: + self.dataframe_to_display = intent_result.data + self.update_timetracking_dataframe() + self.display_dataframe() + self.set_progress_hint(hide_progress=True) """Cloud calendar setup""" From b61f0ec67edd7b60f85fe23b5b79ffa730b66558 Mon Sep 17 00:00:00 2001 From: Christian Staudt Date: Sun, 16 Apr 2023 17:31:39 +0200 Subject: [PATCH 2/5] =?UTF-8?q?Bump=20version:=201.1.0a=20=E2=86=92=201.1.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- tuttle/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index a7c48ea2..462c1601 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.0a +current_version = 1.1.1 commit = True tag = True diff --git a/setup.py b/setup.py index f172759d..2bdf99f8 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,6 @@ packages=find_packages(include=["tuttle", "tuttle.*", "tuttle_tests"]), test_suite="tests", url="https://github.com/tuttle-dev/tuttle", - version="1.1.0", + version="1.1.1", zip_safe=False, ) diff --git a/tuttle/__init__.py b/tuttle/__init__.py index 79e6c112..28843a79 100644 --- a/tuttle/__init__.py +++ b/tuttle/__init__.py @@ -4,7 +4,7 @@ "Christian Staudt", "Vladimir Peter", ] -__version__ = "1.1.0" +__version__ = "1.1.1" from . import app from . import ( From c412bbfd37ffe842cbc3418278a4fbffb16fe36a Mon Sep 17 00:00:00 2001 From: Christian Staudt Date: Sun, 16 Apr 2023 17:46:28 +0200 Subject: [PATCH 3/5] Readme updated --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2899c2d3..a715bb9a 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Track regular expenses, taxes and social security contributions. Estimate them f Calculate your effective income and see how much you can spend without risking your financial security. -## Prototype Test +## Test via Python ### Setup @@ -100,15 +100,22 @@ $ pytest $ python app/app.py ``` -## Installation +## Test the App Bundle + +Download the latest [release](https://github.com/tuttle-dev/tuttle/releases) for your platform. ### macOS +1. Run the .app bundle. + ### Linux +1. Run the executable. + ### Windows 1. Requires [GTK](https://www.gtk.org). +2. Run the .exe file. ## Build From 112a5c4fbc59ae96cf34d7002b9e8d9407174c90 Mon Sep 17 00:00:00 2001 From: Christian Staudt Date: Mon, 24 Apr 2023 11:59:25 +0200 Subject: [PATCH 4/5] pdf preview: raise exception on process call error --- tuttle/os_functions.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tuttle/os_functions.py b/tuttle/os_functions.py index a4c142b6..650c1c63 100644 --- a/tuttle/os_functions.py +++ b/tuttle/os_functions.py @@ -18,16 +18,21 @@ def open_application(app_name): def preview_pdf(file_path): """Preview a PDF file.""" - if not Path(file_path).exists(): - raise FileNotFoundError(f"File not found: {file_path}") - if platform.system() == "Darwin": - os.system("qlmanage -p {}".format(file_path)) - elif platform.system() == "Windows": - os.startfile(file_path) - elif platform.system() == "Linux": - os.system("xdg-open {}".format(file_path)) - else: - print("Sorry, your platform is not supported.") + try: + if not Path(file_path).exists(): + raise FileNotFoundError(f"File not found: {file_path}") + if platform.system() == "Darwin": + subprocess.check_call(["qlmanage", "-p", file_path]) + elif platform.system() == "Windows": + os.startfile(file_path) + elif platform.system() == "Linux": + subprocess.check_call(["xdg-open", file_path]) + else: + raise RuntimeError("Sorry, your platform is not supported.") + except subprocess.CalledProcessError as err: + raise RuntimeError( + f"Error occurred while opening the PDF file. Return code: {err.returncode}. Error: {err.output}" + ) def open_folder(folder_path): From ddd87587fce099cc8dc494389f44607e3dc48518 Mon Sep 17 00:00:00 2001 From: Christian Staudt Date: Sat, 27 Jul 2024 16:57:48 +0200 Subject: [PATCH 5/5] update requirements --- requirements.txt | 2 +- tuttle/schema.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 634dc873..1c4bbc66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pytest -pandas<2.0.0 +pandas matplotlib altair pydantic diff --git a/tuttle/schema.py b/tuttle/schema.py index 8075e57f..b951a5f2 100644 --- a/tuttle/schema.py +++ b/tuttle/schema.py @@ -1,6 +1,6 @@ """Pandera schemata.""" from pandera import ( - SchemaModel, + # SchemaModel, DataFrameSchema, Column, Index,