mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 15:57:59 -05:00
Notivue started + fixes to location on file parsing
[README] added volume for log [frontend] started transition of toasts to Notivue [frontend] added more detailed info on fetch error requests [backend] added validation to not throw an error if impossible to query location data for the activity [backend] added validation on location_based_on_coordinates and raise exception when not possible to query Geocodes endpoint [backend] added additional validations to better handle location missing on parsing .fit and .gpx files
This commit is contained in:
@@ -168,6 +168,7 @@ It is recommended to configure the following volumes for code and data persisten
|
||||
| /app | <local_path>/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 | <local_path>/endurain/backend/app/user_images:/app/user_images | Necessary for user image persistence on container image updates |
|
||||
| /app/bulk_import | <local_path>/endurain/backend/bulk_import:/app/bulk_import | Necessary to enable bulk import of activities. Place here your activities files |
|
||||
| /app/app.log | <local_path>/endurain/backend/app.log:/app/app.log | Log file for the backend |
|
||||
|
||||
## Strava Integration
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
0
backend/app/bulk_import/__init__.py
Normal file
0
backend/app/bulk_import/__init__.py
Normal file
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
23
frontend/app/package-lock.json
generated
23
frontend/app/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Notivue v-slot="item">
|
||||
<Notification :item="item" :theme="pastelTheme">
|
||||
<NotificationProgress :item="item" />
|
||||
</Notification>
|
||||
</Notivue>
|
||||
<NavbarComponent />
|
||||
<ToastContainerComponent />
|
||||
<main class="container py-4 bg-body">
|
||||
|
||||
@@ -78,7 +78,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Replace with your actual API call for activities
|
||||
searchResults.value = await activities.getActivityByName(query);
|
||||
} catch (error) {
|
||||
console.error('Error fetching activity results:', error);
|
||||
@@ -91,7 +90,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Replace with your actual API call for gear
|
||||
searchResults.value = await gears.getGearByNickname(query);
|
||||
} catch (error) {
|
||||
console.error('Error fetching gear results:', error);
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createApp } from "vue";
|
||||
import { createNotivue } from "notivue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
import { useAuthStore } from './stores/authStore';
|
||||
import { useThemeStore } from './stores/themeStore';
|
||||
import { useAuthStore } from "./stores/authStore";
|
||||
import { useThemeStore } from "./stores/themeStore";
|
||||
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||
import "bootstrap/dist/js/bootstrap.bundle.min.js";
|
||||
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import i18n from './i18n';
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import i18n from "./i18n";
|
||||
|
||||
/* import the fontawesome core */
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
/* import font awesome icon component */
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
/* import icons */
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||
import { fab } from '@fortawesome/free-brands-svg-icons';
|
||||
import { far } from '@fortawesome/free-regular-svg-icons';
|
||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||
import { fab } from "@fortawesome/free-brands-svg-icons";
|
||||
import { far } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
/* add icons to the library */
|
||||
library.add(fas, fab, far);
|
||||
|
||||
/* import notivue components */
|
||||
import "notivue/notification.css";
|
||||
import "notivue/animations.css";
|
||||
import "notivue/notification-progress.css";
|
||||
|
||||
const notivue = createNotivue({
|
||||
notifications: {
|
||||
global: {
|
||||
duration: 5000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.component('font-awesome-icon', FontAwesomeIcon);
|
||||
app.use(notivue);
|
||||
app.component("font-awesome-icon", FontAwesomeIcon);
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
|
||||
@@ -40,4 +55,4 @@ authStore.loadUserFromStorage(i18n);
|
||||
const themeStore = useThemeStore();
|
||||
themeStore.loadThemeFromStorage();
|
||||
|
||||
app.mount('#app');
|
||||
app.mount("#app");
|
||||
|
||||
@@ -9,7 +9,7 @@ async function fetchWithRetry(url, options) {
|
||||
await refreshAccessToken();
|
||||
return await attemptFetch(url, options);
|
||||
} catch (refreshError) {
|
||||
throw new Error('Failed to refresh access token: ' + refreshError.message);
|
||||
throw new Error(`Failed to refresh access token: ${refreshError.message}`);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
@@ -21,7 +21,9 @@ async function attemptFetch(url, options) {
|
||||
const fullUrl = `${API_URL}${url}`;
|
||||
const response = await fetch(fullUrl, options);
|
||||
if (!response.ok) {
|
||||
throw new Error('' + response.status);
|
||||
const errorBody = await response.json(); // Parse the response as JSON
|
||||
const errorMessage = errorBody.detail || 'Unknown error'; // Get the 'detail' field or a default message
|
||||
throw new Error(`${response.status} - ${errorMessage}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -37,7 +39,9 @@ async function refreshAccessToken() {
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('' + response.status);
|
||||
const errorBody = await response.json(); // Parse the response as JSON
|
||||
const errorMessage = errorBody.detail || 'Unknown error'; // Get the 'detail' field or a default message
|
||||
throw new Error(`${response.status} - ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,8 +118,8 @@ import { useAuthStore } from '@/stores/authStore';
|
||||
// Import the services
|
||||
import { activities } from '@/services/activitiesService';
|
||||
import { followers } from '@/services/followersService';
|
||||
// Importing the utils
|
||||
import { addToast } from '@/utils/toastUtils';
|
||||
// Import Notivue push
|
||||
import { push } from 'notivue'
|
||||
// Importing the components
|
||||
import UserDistanceStatsComponent from '@/components/Activities/UserDistanceStatsComponent.vue';
|
||||
import NoItemsFoundComponent from '@/components/GeneralComponents/NoItemsFoundComponents.vue';
|
||||
@@ -160,7 +160,7 @@ export default {
|
||||
thisMonthDistances.value = await activities.getUserThisMonthStats(authStore.user.id);
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
addToast(`${t('generalItens.errorFetchingInfo')} - ${error}`, 'danger', true);
|
||||
push.error(`${t('generalItens.errorFetchingInfo')} - ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
addToast(`${t('generalItens.errorFetchingInfo')} - ${error}`, 'danger', true);
|
||||
push.error(`${t('generalItens.errorFetchingInfo')} - ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export default {
|
||||
|
||||
const submitUploadFileForm = async () => {
|
||||
// Set the loading message
|
||||
addToast(t('homeView.processingActivity'), 'loading', true);
|
||||
const notification = push.promise(t('homeView.processingActivity'))
|
||||
|
||||
// Get the file input
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
@@ -218,7 +218,7 @@ export default {
|
||||
userActivities.value.unshift(createdActivity);
|
||||
|
||||
// Set the success message
|
||||
addToast(t('homeView.successActivityAdded'), 'success', true);
|
||||
notification.resolve(t('homeView.successActivityAdded'))
|
||||
|
||||
// Clear the file input
|
||||
fileInput.value = '';
|
||||
@@ -230,7 +230,7 @@ export default {
|
||||
userNumberOfActivities.value ++;
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
addToast(`${t('generalItens.errorFetchingInfo')} - ${error}`, 'danger', true);
|
||||
notification.reject(`${t('generalItens.errorFetchingInfo')} - ${error}`)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -238,13 +238,13 @@ export default {
|
||||
onMounted(async () => {
|
||||
if (route.query.activityFound === 'false') {
|
||||
// Set the activityFound value to false and show the error alert.
|
||||
addToast(`${t('homeView.errorActivityNotFound')} - ${error}`, 'danger', true);
|
||||
push.error(`${t('homeView.errorActivityNotFound')} - ${error}`)
|
||||
}
|
||||
|
||||
if (route.query.activityDeleted === 'true') {
|
||||
userActivities.value = userActivities.value.filter(activity => activity.id !== Number(route.query.activityId));
|
||||
// Set the activityDeleted value to true and show the success alert.
|
||||
addToast(t('homeView.successActivityDeleted'), 'success', true);
|
||||
push.success(t('homeView.successActivityDeleted'))
|
||||
}
|
||||
|
||||
// Add the scroll event listener
|
||||
@@ -265,7 +265,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
addToast(`${t('generalItens.errorFetchingInfo')} - ${error}`, 'danger', true);
|
||||
push.error(`${t('generalItens.errorFetchingInfo')} - ${error}`)
|
||||
}
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
Reference in New Issue
Block a user