diff --git a/README.md b/README.md index d13da9954..cf3199cef 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ It is recommended to configure the following volumes for code and data persisten | /app | /endurain/backend/app:/app | Configure volume if you want to edit the code locally by cloning the repo and comment next line | | /app/user_images | /endurain/backend/app/user_images:/app/user_images | Necessary for user image persistence on container image updates | | /app/bulk_import | /endurain/backend/bulk_import:/app/bulk_import | Necessary to enable bulk import of activities. Place here your activities files | +| /app/app.log | /endurain/backend/app.log:/app/app.log | Log file for the backend | ## Strava Integration diff --git a/backend/app/activities/router.py b/backend/app/activities/router.py index 29f2ccae6..1a0bfaa02 100644 --- a/backend/app/activities/router.py +++ b/backend/app/activities/router.py @@ -425,10 +425,6 @@ async def create_activity_with_uploaded_file( ], ): try: - # Ensure the 'uploads' directory exists - upload_dir = "uploads" - os.makedirs(upload_dir, exist_ok=True) - # Return activity return activies_utils.parse_and_store_activity_from_uploaded_file(token_user_id, file, db) except Exception as err: @@ -437,10 +433,7 @@ async def create_activity_with_uploaded_file( f"Error in create_activity_with_uploaded_file: {err}", exc_info=True ) # Raise an HTTPException with a 500 Internal Server Error status code - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal Server Error", - ) from err + raise err @router.post( diff --git a/backend/app/activities/utils.py b/backend/app/activities/utils.py index 1e0436da7..7310ef8ab 100644 --- a/backend/app/activities/utils.py +++ b/backend/app/activities/utils.py @@ -36,20 +36,21 @@ async def parse_and_store_activity_from_file( # Parse the file parsed_info = parse_file(token_user_id, file_extension, file_path) - # Store the activity in the database - created_activity = store_activity(parsed_info, db) + if parsed_info is not None: + # Store the activity in the database + created_activity = store_activity(parsed_info, db) - # Return the created activity - return created_activity + # Return the created activity + return created_activity + else: + return None + except HTTPException: + pass except Exception as err: # Log the exception logger.error( f"Error in parse_and_store_activity_from_file: {err}", exc_info=True ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal Server Error", - ) from err def parse_and_store_activity_from_uploaded_file( @@ -59,38 +60,71 @@ def parse_and_store_activity_from_uploaded_file( # Get file extension _, file_extension = os.path.splitext(file.filename) - # Save the uploaded file in the 'uploads' directory - with open(file.filename, "wb") as save_file: - save_file.write(file.file.read()) + try: + # Ensure the 'uploads' directory exists + upload_dir = "uploads" + os.makedirs(upload_dir, exist_ok=True) - # Parse the file - parsed_info = parse_file(token_user_id, file_extension, file.filename) + # Build the full path where the file will be saved + file_path = os.path.join(upload_dir, file.filename) - # Store the activity in the database - created_activity = store_activity(parsed_info, db) + # Save the uploaded file in the 'uploads' directory + with open(file_path, "wb") as save_file: + save_file.write(file.file.read()) - # Return the created activity - return created_activity + # Parse the file + parsed_info = parse_file(token_user_id, file_extension, file_path) + + if parsed_info is not None: + # Store the activity in the database + created_activity = store_activity(parsed_info, db) + + # Return the created activity + return created_activity + else: + return None + except HTTPException as http_err: + raise http_err + except Exception as err: + # Log the exception + logger.error(f"Error in parse_and_store_activity_from_uploaded_file: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + finally: + # Remove the file after processing + os.remove(file_path) def parse_file(token_user_id: int, file_extension: str, filename: str) -> dict: try: - # Choose the appropriate parser based on file extension - if file_extension.lower() == ".gpx": - # Parse the GPX file - parsed_info = gpx_utils.parse_gpx_file(filename, token_user_id) - elif file_extension.lower() == ".fit": - # Parse the FIT file - parsed_info = fit_utils.parse_fit_file(filename, token_user_id) - else: - # file extension not supported raise an HTTPException with a 406 Not Acceptable status code - raise HTTPException( - status_code=status.HTTP_406_NOT_ACCEPTABLE, - detail="File extension not supported. Supported file extensions are .gpx and .fit", - ) + if filename.lower() != "bulk_import/__init__.py": + print("Parsing file: ", filename) + logger.info(f"Parsing file: {filename}") + # Choose the appropriate parser based on file extension + if file_extension.lower() == ".gpx": + # Parse the GPX file + parsed_info = gpx_utils.parse_gpx_file(filename, token_user_id) + elif file_extension.lower() == ".fit": + # Parse the FIT file + parsed_info = fit_utils.parse_fit_file(filename, token_user_id) + else: + # file extension not supported raise an HTTPException with a 406 Not Acceptable status code + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, + detail="File extension not supported. Supported file extensions are .gpx and .fit", + ) + + # Remove the file after processing + os.remove(filename) - # Return the parsed information - return parsed_info + return parsed_info + else: + return None + except HTTPException as http_err: + raise http_err except Exception as err: # Log the exception logger.error(f"Error in parse_file: {err}", exc_info=True) @@ -99,9 +133,6 @@ def parse_file(token_user_id: int, file_extension: str, filename: str) -> dict: status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal Server Error", ) from err - finally: - # Remove the file after processing - os.remove(filename) def store_activity(parsed_info: dict, db: Session): @@ -110,6 +141,10 @@ def store_activity(parsed_info: dict, db: Session): # Check if created_activity is None if created_activity is None: + # Log the error + logger.error( + "Error in store_activity - activity is None, error creating activity" + ) # raise an HTTPException with a 500 Internal Server Error status code raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -150,6 +185,9 @@ def calculate_activity_distances(activities: list[activities_schema.Activity]): def location_based_on_coordinates(latitude, longitude) -> dict | None: if latitude is None or longitude is None: return None + + if os.environ.get("GEOCODES_MAPS_API") == "changeme1": + return None # Create a dictionary with the parameters for the request url_params = { @@ -176,12 +214,15 @@ def location_based_on_coordinates(latitude, longitude) -> dict | None: "town": data.get("town"), "country": data.get("country"), } - except requests.exceptions.RequestException as err: # Log the error logger.error( - f"Error in upload_file querying local from geocode: {err}", - exc_info=True, + "Error in location_based_on_coordinates - Invalid API key provided for the geocoding service" + ) + print("Error in location_based_on_coordinates - Invalid API key provided for the geocoding service") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid API key provided for the geocoding service", ) diff --git a/backend/app/bulk_import/__init__.py b/backend/app/bulk_import/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/app/fit/utils.py b/backend/app/fit/utils.py index 047bb4b42..4f152252e 100644 --- a/backend/app/fit/utils.py +++ b/backend/app/fit/utils.py @@ -192,11 +192,13 @@ def parse_fit_file(file: str, user_id: int) -> dict: location_data = activities_utils.location_based_on_coordinates( latitude, longitude ) - city = location_data["city"] - town = location_data["town"] - country = location_data["country"] + + if location_data: + city = location_data["city"] + town = location_data["town"] + country = location_data["country"] - process_one_time_fields = 1 + process_one_time_fields = 1 # Check if heart rate, cadence, power are set if heart_rate != 0: diff --git a/backend/app/gpx/utils.py b/backend/app/gpx/utils.py index 2bbe2a153..9a53c2018 100644 --- a/backend/app/gpx/utils.py +++ b/backend/app/gpx/utils.py @@ -76,6 +76,8 @@ def parse_gpx_file(file: str, user_id: int) -> dict: country = None process_one_time_fields = 0 pace = 0 + calories = 0 + visibility = 0 # Arrays to store waypoint data lat_lon_waypoints = [] @@ -129,11 +131,13 @@ def parse_gpx_file(file: str, user_id: int) -> dict: location_data = activities_utils.location_based_on_coordinates( latitude, longitude ) - city = location_data["city"] - town = location_data["town"] - country = location_data["country"] - process_one_time_fields = 1 + if location_data: + city = location_data["city"] + town = location_data["town"] + country = location_data["country"] + + process_one_time_fields = 1 # Extract heart rate, cadence, and power data from point extensions heart_rate, cadence, power = 0, 0, 0 @@ -265,6 +269,8 @@ def parse_gpx_file(file: str, user_id: int) -> dict: pace=pace, average_speed=average_speed, average_power=average_power, + calories=calories, + visibility=visibility, strava_gear_id=None, strava_activity_id=None, ) diff --git a/frontend/app/package-lock.json b/frontend/app/package-lock.json index 727eb2ccd..3f599ed4c 100644 --- a/frontend/app/package-lock.json +++ b/frontend/app/package-lock.json @@ -16,6 +16,7 @@ "bootstrap": "^5.3.2", "chart.js": "^4.4.1", "leaflet": "^1.9.4", + "notivue": "^2.4.5", "pinia": "^2.1.7", "vue": "^3.4.15", "vue-i18n": "^9.9.1", @@ -6292,6 +6293,28 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/notivue": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/notivue/-/notivue-2.4.5.tgz", + "integrity": "sha512-7yBdaKesUZIwdcQP3nv1oWYyisI2bURkZ+D9KfLgeNqguHUzkQ1WdhGcTj59PBZa8mqa1/K5Mh8YsphSToMKcQ==", + "license": "MIT", + "peerDependencies": { + "@nuxt/kit": ">=3.5.0", + "@nuxt/schema": ">=3.5.0", + "defu": ">=6" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@nuxt/schema": { + "optional": true + }, + "defu": { + "optional": true + } + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", diff --git a/frontend/app/package.json b/frontend/app/package.json index f70c4c6f0..4b882a348 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -20,6 +20,7 @@ "bootstrap": "^5.3.2", "chart.js": "^4.4.1", "leaflet": "^1.9.4", + "notivue": "^2.4.5", "pinia": "^2.1.7", "vue": "^3.4.15", "vue-i18n": "^9.9.1", diff --git a/frontend/app/src/App.vue b/frontend/app/src/App.vue index 01977e767..e03f2e7f6 100644 --- a/frontend/app/src/App.vue +++ b/frontend/app/src/App.vue @@ -3,9 +3,15 @@ import ToastContainerComponent from './components/Toasts/ToastContainerComponent.vue' import NavbarComponent from './components/Navbar/NavbarComponent.vue' import FooterComponent from './components/FooterComponent.vue' + import { Notivue, Notification, NotificationProgress, pastelTheme } from 'notivue'