mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-10 08:17:59 -05:00
Merge branch 'pr/285' into 0.15.0
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -41,12 +41,18 @@ backend/app/data/activity_files/*.tcx
|
||||
backend/app/data/activity_files/bulk_import/*.gpx
|
||||
backend/app/data/activity_files/bulk_import/*.fit
|
||||
backend/app/data/activity_files/bulk_import/*.tcx
|
||||
backend/app/data/activity_files/bulk_import/*.gz
|
||||
backend/app/data/activity_files/bulk_import/*.csv
|
||||
backend/app/data/activity_files/bulk_import/import_errors/*.gpx
|
||||
backend/app/data/activity_files/bulk_import/import_errors/*.fit
|
||||
backend/app/data/activity_files/bulk_import/import_errors/*.tcx
|
||||
backend/app/data/activity_files/bulk_import/import_errors/*.gz
|
||||
backend/app/data/activity_files/bulk_import/import_errors/*.csv
|
||||
backend/app/data/activity_files/processed/*.gpx
|
||||
backend/app/data/activity_files/processed/*.fit
|
||||
backend/app/data/activity_files/processed/*.tcx
|
||||
backend/app/data/activity_files/processed/*.gz
|
||||
backend/app/data/activity_files/processed/*.csv
|
||||
backend/data/activity_files/
|
||||
|
||||
# Frontend
|
||||
|
||||
@@ -22,6 +22,7 @@ ACTIVITY_MEDIA_DIR = os.getenv("ACTIVITY_MEDIA_DIR", f"{DATA_DIR}/activity_media
|
||||
FILES_PROCESSED_DIR = f"{FILES_DIR}/processed"
|
||||
FILES_BULK_IMPORT_DIR = f"{FILES_DIR}/bulk_import"
|
||||
FILES_BULK_IMPORT_IMPORT_ERRORS_DIR = f"{FILES_BULK_IMPORT_DIR}/import_errors"
|
||||
STRAVA_BULK_IMPORT_BIKES_FILE = "bikes.csv"
|
||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "production").lower()
|
||||
TZ = os.getenv("TZ", "UTC")
|
||||
REVERSE_GEO_PROVIDER = os.getenv("REVERSE_GEO_PROVIDER", "nominatim").lower()
|
||||
|
||||
@@ -296,19 +296,54 @@ def get_gear_by_garminconnect_id_from_user_id(
|
||||
|
||||
def create_multiple_gears(gears: list[gears_schema.Gear], user_id: int, db: Session):
|
||||
try:
|
||||
# Filter out None values from the gears list
|
||||
valid_gears = [gear for gear in gears if gear is not None]
|
||||
|
||||
# Create a list of gear objects
|
||||
new_gears = [
|
||||
gears_utils.transform_schema_gear_to_model_gear(gear, user_id)
|
||||
for gear in valid_gears
|
||||
# 1) Filter out None and gears without a usable nickname
|
||||
valid_gears = [
|
||||
gear
|
||||
for gear in (gears or [])
|
||||
if gear is not None
|
||||
and getattr(gear, "nickname", None)
|
||||
and str(gear.nickname).strip()
|
||||
]
|
||||
|
||||
# Add the gears to the database
|
||||
db.add_all(new_gears)
|
||||
db.commit()
|
||||
# 2) De-dupe within the valid_gears payload (case-insensitive, trimmed)
|
||||
seen = set()
|
||||
deduped: list[gears_schema.Gear] = []
|
||||
for gear in valid_gears:
|
||||
nickname_normalized = str(gear.nickname).replace("+", " ").strip()
|
||||
if nickname_normalized not in seen:
|
||||
seen.add(nickname_normalized)
|
||||
deduped.append(gear)
|
||||
else:
|
||||
core_logger.print_to_log_and_console(
|
||||
f"Duplicate nickname '{gear.nickname}' in request for user {user_id}, skipping",
|
||||
"warning",
|
||||
)
|
||||
|
||||
# 3) Skip any that already exist for this user
|
||||
gears_to_create: list[gears_schema.Gear] = []
|
||||
for gear in deduped:
|
||||
gear_check = get_gear_user_by_nickname(user_id, gear.nickname, db)
|
||||
|
||||
if gear_check is not None:
|
||||
core_logger.print_to_log_and_console(
|
||||
f"Gear with nickname '{gear.nickname}' already exists for user {user_id}, skipping",
|
||||
"warning",
|
||||
)
|
||||
else:
|
||||
gears_to_create.append(gear)
|
||||
|
||||
# 4) Persist any remaining
|
||||
if gears_to_create:
|
||||
new_gears = [
|
||||
gears_utils.transform_schema_gear_to_model_gear(gear, user_id)
|
||||
for gear in gears_to_create
|
||||
]
|
||||
db.add_all(new_gears)
|
||||
db.commit()
|
||||
|
||||
except HTTPException as http_err:
|
||||
# If an HTTPException is raised, re-raise it
|
||||
raise http_err
|
||||
except IntegrityError as integrity_error:
|
||||
# Rollback the transaction
|
||||
db.rollback()
|
||||
@@ -316,9 +351,8 @@ def create_multiple_gears(gears: list[gears_schema.Gear], user_id: int, db: Sess
|
||||
# Raise an HTTPException with a 500 Internal Server Error status code
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Duplicate entry error. Check if nickname, strava_gear_id or garminconnect_gear_id are unique",
|
||||
detail="Duplicate entry error. Check if strava_gear_id or garminconnect_gear_id are unique",
|
||||
) from integrity_error
|
||||
|
||||
except Exception as err:
|
||||
# Rollback the transaction
|
||||
db.rollback()
|
||||
@@ -337,9 +371,7 @@ def create_multiple_gears(gears: list[gears_schema.Gear], user_id: int, db: Sess
|
||||
|
||||
def create_gear(gear: gears_schema.Gear, user_id: int, db: Session):
|
||||
try:
|
||||
gear_check = get_gear_user_by_nickname(
|
||||
user_id, gear.nickname, db
|
||||
)
|
||||
gear_check = get_gear_user_by_nickname(user_id, gear.nickname, db)
|
||||
|
||||
if gear_check is not None:
|
||||
# If the gear already exists, raise an HTTPException with a 409 Conflict status code
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
from sqlalchemy import func
|
||||
from urllib.parse import unquote
|
||||
from typing import Annotated
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
import session.security as session_security
|
||||
import core.database as core_database
|
||||
|
||||
import gears.gear.models as gears_models
|
||||
import gears.gear.crud as gears_crud
|
||||
import gears.gear.schema as gears_schema
|
||||
|
||||
# Global gear type integer to gear name mapping (ID to name)
|
||||
@@ -37,6 +43,7 @@ GEAR_NAME_TO_ID.update(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def transform_schema_gear_to_model_gear(
|
||||
gear: gears_schema.Gear, user_id: int
|
||||
) -> gears_models.Gear:
|
||||
@@ -50,12 +57,16 @@ def transform_schema_gear_to_model_gear(
|
||||
# Create a new gear object
|
||||
new_gear = gears_models.Gear(
|
||||
brand=(
|
||||
unquote(gear.brand).replace("+", " ") if gear.brand is not None else None
|
||||
unquote(gear.brand).replace("+", " ").strip()
|
||||
if gear.brand is not None
|
||||
else None
|
||||
),
|
||||
model=(
|
||||
unquote(gear.model).replace("+", " ") if gear.model is not None else None
|
||||
unquote(gear.model).replace("+", " ").strip()
|
||||
if gear.model is not None
|
||||
else None
|
||||
),
|
||||
nickname=unquote(gear.nickname).replace("+", " "),
|
||||
nickname=unquote(gear.nickname).replace("+", " ").strip(),
|
||||
gear_type=gear.gear_type,
|
||||
user_id=user_id,
|
||||
created_at=created_date,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import os
|
||||
import csv
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from stravalib.client import Client
|
||||
|
||||
import core.config as core_config
|
||||
import core.logger as core_logger
|
||||
|
||||
import strava.utils as strava_utils
|
||||
@@ -9,6 +12,7 @@ import strava.athlete_utils as strava_athlete_utils
|
||||
|
||||
import gears.gear.schema as gears_schema
|
||||
import gears.gear.crud as gears_crud
|
||||
import gears.gear.utils as gears_utils
|
||||
|
||||
import activities.activity.schema as activities_schema
|
||||
import activities.activity.crud as activities_crud
|
||||
@@ -32,7 +36,7 @@ def get_strava_gear(gear_id: str, strava_client: Client):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
||||
detail="Not able to fetch Strava gear",
|
||||
)
|
||||
) from err
|
||||
|
||||
if strava_gear is None:
|
||||
raise HTTPException(
|
||||
@@ -80,7 +84,7 @@ def fetch_and_process_gear(strava_client: Client, user_id: int, db: Session) ->
|
||||
|
||||
|
||||
def process_gear(
|
||||
gear, type: str, user_id: int, strava_client: Client, db: Session
|
||||
gear, gear_type: str, user_id: int, strava_client: Client, db: Session
|
||||
) -> gears_schema.Gear | None:
|
||||
# Get the gear by strava id from user id
|
||||
gear_db = gears_crud.get_gear_by_strava_id_from_user_id(gear.id, user_id, db)
|
||||
@@ -99,7 +103,7 @@ def process_gear(
|
||||
brand=strava_gear.brand_name,
|
||||
model=strava_gear.model_name,
|
||||
nickname=strava_gear.name,
|
||||
gear_type=1 if type == "bike" else 2,
|
||||
gear_type=1 if gear_type == "bike" else 2,
|
||||
user_id=user_id,
|
||||
active=True,
|
||||
strava_gear_id=gear.id,
|
||||
@@ -109,11 +113,7 @@ def process_gear(
|
||||
|
||||
|
||||
def iterate_over_activities_and_set_gear(
|
||||
activity: activities_schema.Activity,
|
||||
gears: list[gears_schema.Gear],
|
||||
counter: int,
|
||||
user_id: int,
|
||||
db: Session,
|
||||
activity: activities_schema.Activity, gears: list[gears_schema.Gear], counter: int
|
||||
) -> dict:
|
||||
|
||||
# Iterate over gears and set gear if applicable
|
||||
@@ -151,9 +151,7 @@ def set_activities_gear(user_id: int, db: Session) -> int:
|
||||
|
||||
# iterate over activities and set gear if applicable
|
||||
for activity in activities:
|
||||
parsed_activity = iterate_over_activities_and_set_gear(
|
||||
activity, gears, counter, user_id, db
|
||||
)
|
||||
parsed_activity = iterate_over_activities_and_set_gear(activity, gears, counter)
|
||||
counter = parsed_activity["counter"]
|
||||
activities_parsed.append(parsed_activity["activity"])
|
||||
|
||||
@@ -202,3 +200,95 @@ def get_user_gear(user_id: int):
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def iterate_over_bikes_csv() -> dict:
|
||||
"""
|
||||
Parses a Strava bikes CSV file and returns its contents as a dictionary.
|
||||
|
||||
The function looks for a CSV file specified by configuration settings, reads it, and constructs a dictionary where each key is the "Bike Name" from the CSV, and the value is a dictionary of the bike's attributes as provided in the CSV row.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping bike names to their corresponding data from the CSV file.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the CSV file is missing, has invalid headers, or cannot be parsed.
|
||||
"""
|
||||
# CSV file location
|
||||
bulk_import_dir = core_config.FILES_BULK_IMPORT_DIR
|
||||
bikes_file_name = core_config.STRAVA_BULK_IMPORT_BIKES_FILE
|
||||
bikes_file_path = os.path.join(bulk_import_dir, bikes_file_name)
|
||||
|
||||
# Get file and parse it
|
||||
bikes_dict = {}
|
||||
try:
|
||||
if os.path.isfile(bikes_file_path):
|
||||
core_logger.print_to_log_and_console(
|
||||
f"{bikes_file_name} exists in the {bulk_import_dir} directory. Starting to process file."
|
||||
)
|
||||
with open(bikes_file_path, "r", encoding="utf-8") as bike_file:
|
||||
bikes_csv = csv.DictReader(bike_file)
|
||||
for row in bikes_csv:
|
||||
if (
|
||||
("Bike Name" not in row)
|
||||
or ("Bike Brand" not in row)
|
||||
or ("Bike Model" not in row)
|
||||
):
|
||||
core_logger.print_to_log_and_console(
|
||||
f"Aborting bikes import: Proper headers not found in {bikes_file_name}. File should have 'Bike Name', 'Bike Brand', and 'Bike Model'."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
||||
detail="Invalid file. Proper headers not found in Strava bikes CSV file.",
|
||||
)
|
||||
bikes_dict[row["Bike Name"]] = row
|
||||
core_logger.print_to_log_and_console(
|
||||
f"Strava bike gear csv file parsed and gear dictionary created. File was {len(bikes_dict)} rows long, ignoring header row."
|
||||
)
|
||||
return bikes_dict
|
||||
core_logger.print_to_log_and_console(
|
||||
f"No {bikes_file_name} file located in the {bulk_import_dir} directory."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
||||
detail="No Strava bikes CSV file found for import.",
|
||||
)
|
||||
except HTTPException as http_err:
|
||||
raise http_err
|
||||
except Exception as err:
|
||||
core_logger.print_to_log_and_console(
|
||||
f"Error attempting to open {bikes_file_path} file: {err}", "error"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
||||
detail="Error parsing Strava bikes CSV file.",
|
||||
) from err
|
||||
|
||||
|
||||
def transform_csv_bike_gear_to_schema_gear(
|
||||
bikes_dict: dict, token_user_id: int
|
||||
) -> list[gears_schema.Gear]:
|
||||
"""
|
||||
Transforms a dictionary of bike data (parsed from CSV) into a list of Gear schema objects.
|
||||
|
||||
Args:
|
||||
bikes_dict (dict): A dictionary where each key is a bike nickname and each value is a dictionary
|
||||
containing bike attributes such as "Bike Brand" and "Bike Model".
|
||||
token_user_id (int): The user ID to associate with each Gear object.
|
||||
|
||||
Returns:
|
||||
list[gears_schema.Gear]: A list of Gear schema objects created from the input bike data.
|
||||
"""
|
||||
gears = []
|
||||
for bike in bikes_dict:
|
||||
new_gear = gears_schema.Gear(
|
||||
user_id=token_user_id,
|
||||
brand=bikes_dict[bike]["Bike Brand"],
|
||||
model=bikes_dict[bike]["Bike Model"],
|
||||
nickname=bike,
|
||||
gear_type=gears_utils.GEAR_NAME_TO_ID["bike"],
|
||||
is_active=True,
|
||||
strava_gear_id=None,
|
||||
)
|
||||
gears.append(new_gear)
|
||||
return gears
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated, Callable
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Security
|
||||
@@ -10,14 +11,17 @@ import session.security as session_security
|
||||
import users.user_integrations.crud as user_integrations_crud
|
||||
|
||||
import gears.gear.crud as gears_crud
|
||||
import gears.gear.utils as gears_utils
|
||||
|
||||
import activities.activity.crud as activities_crud
|
||||
import activities.activity.utils as activities_utils
|
||||
|
||||
import strava.gear_utils as strava_gear_utils
|
||||
import strava.activity_utils as strava_activity_utils
|
||||
import strava.utils as strava_utils
|
||||
import strava.schema as strava_schema
|
||||
|
||||
import core.config as core_config
|
||||
import core.cryptography as core_cryptography
|
||||
import core.logger as core_logger
|
||||
import core.database as core_database
|
||||
@@ -67,8 +71,12 @@ async def strava_link(
|
||||
|
||||
# Exchange code for token
|
||||
tokens = strava_client.exchange_code_for_token(
|
||||
client_id=core_cryptography.decrypt_token_fernet(user_integrations.strava_client_id),
|
||||
client_secret=core_cryptography.decrypt_token_fernet(user_integrations.strava_client_secret),
|
||||
client_id=core_cryptography.decrypt_token_fernet(
|
||||
user_integrations.strava_client_id
|
||||
),
|
||||
client_secret=core_cryptography.decrypt_token_fernet(
|
||||
user_integrations.strava_client_secret
|
||||
),
|
||||
code=code,
|
||||
)
|
||||
|
||||
@@ -85,9 +93,7 @@ async def strava_link(
|
||||
)
|
||||
|
||||
# Clean up by setting Strava
|
||||
user_integrations_crud.unlink_strava_account(
|
||||
user_integrations.user_id, db
|
||||
)
|
||||
user_integrations_crud.unlink_strava_account(user_integrations.user_id, db)
|
||||
|
||||
# Raise an HTTPException with appropriate status code
|
||||
raise HTTPException(
|
||||
@@ -140,7 +146,7 @@ async def strava_retrieve_activities_days(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/gear", status_code=202)
|
||||
@router.get("/gear", status_code=201)
|
||||
async def strava_retrieve_gear(
|
||||
validate_access_token: Annotated[
|
||||
Callable,
|
||||
@@ -171,6 +177,59 @@ async def strava_retrieve_gear(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/import/bikes", status_code=201)
|
||||
async def import_bikes_from_strava_export(
|
||||
token_user_id: Annotated[
|
||||
int,
|
||||
Depends(session_security.get_user_id_from_access_token),
|
||||
],
|
||||
db: Annotated[
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
try:
|
||||
# Log beginning of bike import
|
||||
core_logger.print_to_log("Entering bike importing function")
|
||||
|
||||
# Get bikes from Strava export CSV file
|
||||
bikes_dict = strava_gear_utils.iterate_over_bikes_csv()
|
||||
|
||||
# Transform bikes dict to list of Gear schema objects
|
||||
if bikes_dict:
|
||||
bikes = strava_gear_utils.transform_csv_bike_gear_to_schema_gear(
|
||||
bikes_dict, token_user_id
|
||||
)
|
||||
|
||||
# Add bikes to the database
|
||||
if bikes:
|
||||
gears_crud.create_multiple_gears(bikes, token_user_id, db)
|
||||
|
||||
# Define variables for moving the bikes file
|
||||
processed_dir = core_config.FILES_PROCESSED_DIR
|
||||
bulk_import_dir = core_config.FILES_BULK_IMPORT_DIR
|
||||
bikes_file_name = core_config.STRAVA_BULK_IMPORT_BIKES_FILE
|
||||
bikes_file_path = os.path.join(bulk_import_dir, bikes_file_name)
|
||||
|
||||
# Move the bikes file to the processed directory
|
||||
activities_utils.move_file(processed_dir, bikes_file_name, bikes_file_path)
|
||||
|
||||
# Log completion of bike import
|
||||
core_logger.print_to_log_and_console("Bike import complete.")
|
||||
except HTTPException as http_err:
|
||||
raise http_err
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log_and_console(
|
||||
f"Error in import_bikes_from_strava_export: {err}", "error"
|
||||
)
|
||||
# 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
|
||||
|
||||
|
||||
@router.put("/client")
|
||||
async def strava_set_user_client(
|
||||
client: strava_schema.StravaClient,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated, Callable
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Security
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import users.user_default_gear.schema as user_default_gear_schema
|
||||
@@ -40,4 +40,6 @@ async def edit_user_default_gear(
|
||||
],
|
||||
):
|
||||
# Update the user default gear in the database
|
||||
return user_default_gear_crud.edit_user_default_gear(user_default_gear, token_user_id, db)
|
||||
return user_default_gear_crud.edit_user_default_gear(
|
||||
user_default_gear, token_user_id, db
|
||||
)
|
||||
|
||||
@@ -88,8 +88,8 @@ Docker image uses a non-root user, so ensure target folders are not owned by roo
|
||||
## Bulk import and file upload
|
||||
|
||||
To perform a bulk import:
|
||||
- Place .fit, .tcx, .gz and/or .gpx files into the activity_files/bulk_import folder. Create the folder if needed.
|
||||
- In the "Settings" menu select "Integrations".
|
||||
- Place .fit, .tcx, .gz and/or .gpx files into the data/activity_files/bulk_import folder. Create the folder if needed.
|
||||
- In the "Settings" menu select "Import".
|
||||
- Click "Import" next to "Bulk Import".
|
||||
|
||||
.fit files are preferred. I noticed that Strava/Garmin Connect process of converting .fit to .gpx introduces additional data to the activity file leading to minor variances in the data, like for example additional
|
||||
@@ -99,8 +99,30 @@ meters in distance and elevation gain. Some notes:
|
||||
- GEOCODES API has a limit of 1 Request/Second on the free plan, so if you have a large number of files, it might not be possible to import all in the same action
|
||||
- The bulk import currently only imports data present in the .fit, .tcx or .gpx files - no metadata or other media are imported.
|
||||
|
||||
## Importing information from a Strava bulk export (BETA)
|
||||
|
||||
Strava allows users to create a bulk export of their historical activity on the site. This information is stored in a zip file, primarily as .csv files, GPS recording files (e.g., .gpx, .fit), and media files (e.g., .jpg, .png).
|
||||
|
||||
### Importing gear from a Strava bulk import
|
||||
|
||||
At the present time, importing bikes from a Strava bulk export is implemented as a beta feature - use with caution. Components of bikes are not imported - just the bikes themselves.
|
||||
|
||||
To perform an import of bikes:
|
||||
- Place the bikes.csv file from a Strava bulk export into the data/activity_files/bulk_import folder. Create the folder if needed.
|
||||
- In the "Settings" menu select "Import".
|
||||
- Click "Bikes Import" next to "Strava import".
|
||||
- Status messages about the import, including why any gear was not imported, can be found in the logs.
|
||||
|
||||
Ensure the file is named "bikes.csv" and has a header row with at least the fields 'Bike Name', 'Bike Brand', and 'Bike Model'.
|
||||
|
||||
### Importing other items from a Strava bulk import
|
||||
|
||||
Importing of shoes is under development in September 2025.
|
||||
|
||||
Importing activity metadata and media is under development in September 2025.
|
||||
|
||||
## Image personalization
|
||||
|
||||
It is possible (v0.10.0 or higher) to personalize the login image in the login page. To do that, map the data/server_images directory for image persistence on container updates and:
|
||||
- Set the image in the server settings zone of the settings page
|
||||
- A square image is expected. Default one uses 1000px vs 1000px
|
||||
- A square image is expected. Default one uses 1000px vs 1000px
|
||||
|
||||
90
frontend/app/src/components/Settings/SettingsImportZone.vue
Normal file
90
frontend/app/src/components/Settings/SettingsImportZone.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="col">
|
||||
<div class="bg-body-tertiary rounded p-3 shadow-sm">
|
||||
<!-- list zone -->
|
||||
<ul class="list-group list-group-flush">
|
||||
<!-- bulk import zone -->
|
||||
<li class="list-group-item d-flex justify-content-between bg-body-tertiary px-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<font-awesome-icon :icon="['fas', 'file-import']" size="2x" />
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
{{ $t("settingsImportZone.bulkImportIntegrationTitle") }}
|
||||
</div>
|
||||
{{ $t("settingsImportZone.bulkImportIntegrationBody") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- import button -->
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-primary"
|
||||
role="button"
|
||||
@click="submitBulkImport">{{ $t("settingsImportZone.buttonBulkImport") }}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<!-- Strava bulk-export import zone -->
|
||||
<li class="list-group-item d-flex justify-content-between bg-body-tertiary px-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<font-awesome-icon :icon="['fas', 'file-import']" size="2x" />
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
{{ $t("settingsImportZone.stravaGearImportTitle") }}
|
||||
{{ $t('generalItems.betaTag') }}
|
||||
</div>
|
||||
{{ $t("settingsImportZone.stravaGearImportBody") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- import button -->
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-primary"
|
||||
role="button"
|
||||
@click="submitStravaBikesImport">{{ $t("settingsImportZone.stravaGearImportbuttonBikes") }}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
// Import Notivue push
|
||||
import { push } from "notivue";
|
||||
// Importing the services
|
||||
import { activities } from "@/services/activitiesService";
|
||||
import { strava as stravaService } from "@/services/stravaService";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
async function submitBulkImport() {
|
||||
try {
|
||||
await activities.bulkImportActivities();
|
||||
// Show the loading alert.
|
||||
push.info(t("settingsImportZone.loadingMessageBulkImport"));
|
||||
} catch (error) {
|
||||
// If there is an error, show the error alert.
|
||||
push.error(
|
||||
`${t("settingsImportZone.errorMessageUnableToImportActivities")} - ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
async function submitStravaBikesImport() {
|
||||
// Set the loading message
|
||||
const notification = push.promise(t('settingsImportZone.loadingMessageStravaBikesImport'))
|
||||
try {
|
||||
await stravaService.importBikes();
|
||||
// Resolve the loading message with a success message
|
||||
notification.resolve(t("settingsImportZone.successMessageStravaBikesImport"));
|
||||
} catch (error) {
|
||||
// Reject the loading message with an error message
|
||||
notification.reject(
|
||||
`${t("settingsImportZone.errorMessageUnableToImportBikes")} - ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -86,24 +86,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<!-- bulk import zone -->
|
||||
<li class="list-group-item d-flex justify-content-between bg-body-tertiary px-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<font-awesome-icon :icon="['fas', 'file-import']" size="2x" />
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
{{ $t('settingsIntegrationsZone.bulkImportIntegrationTitle') }}
|
||||
</div>
|
||||
{{ $t('settingsIntegrationsZone.bulkImportIntegrationBody') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- import button -->
|
||||
<a href="#" class="btn btn-primary" role="button" @click="submitBulkImport">{{
|
||||
$t('settingsIntegrationsZone.buttonBulkImport')
|
||||
}}</a>
|
||||
</div>
|
||||
</li>
|
||||
<!-- Garmin Connect zone -->
|
||||
<li class="list-group-item d-flex justify-content-between bg-body-tertiary px-0 pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -378,15 +360,6 @@ async function buttonStravaUnlink() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBulkImport() {
|
||||
try {
|
||||
await activities.bulkImportActivities()
|
||||
push.info(t('settingsIntegrationsZone.loadingMessageBulkImport'))
|
||||
} catch (error) {
|
||||
push.error(`${t('settingsIntegrationsZone.errorMessageUnableToImportActivities')} - ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRetrieveGarminConnectActivitiesDays(days) {
|
||||
try {
|
||||
const endDate = new Date()
|
||||
|
||||
@@ -84,6 +84,17 @@
|
||||
<span class="ms-1">{{ $t('settingsSideBar.integrationsSection') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
class="nav-link link-body-emphasis"
|
||||
:class="{ active: activeSection === 'import' }"
|
||||
@click.prevent="changeActive('import')"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'fa-upload']" />
|
||||
<span class="ms-1">{{ $t("settingsSideBar.importSection") }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,6 +60,7 @@ const componentPaths = {
|
||||
settingsUserProfileZone: 'components/settings/settingsUserProfileZoneComponent.json',
|
||||
settingsSecurityZone: 'components/settings/settingsSecurityZoneComponent.json',
|
||||
settingsIntegrationsZone: 'components/settings/settingsIntegrationsZoneComponent.json',
|
||||
settingsImportZone: 'components/settings/settingsImportZoneComponent.json',
|
||||
garminConnectLoginModalComponent:
|
||||
'components/settings/settingsIntegrationsZone/garminConnectLoginModalComponent.json',
|
||||
usersAddEditUserModalComponent:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"bulkImportIntegrationTitle": "Bulk import",
|
||||
"bulkImportIntegrationBody": "Bulk import activities from files (in the data/activity_files/bulk_import folder)",
|
||||
"buttonBulkImport": "Import activities",
|
||||
"loadingMessageBulkImport": "Importing activities from files...",
|
||||
"errorMessageUnableToImportActivities": "An error occurred while importing activities",
|
||||
"stravaGearImportTitle": "Strava gear import",
|
||||
"stravaGearImportBody": "Import gear from a Strava bulk export (in the data/activity_files/bulk_import folder)",
|
||||
"stravaGearImportbuttonBikes": "Import Strava bikes",
|
||||
"loadingMessageStravaBikesImport": "Importing Strava bikes from file...",
|
||||
"successMessageStravaBikesImport": "Strava bikes imported successfully",
|
||||
"errorMessageUnableToImportBikes": "An error occurred while importing Strava bikes"
|
||||
}
|
||||
@@ -27,11 +27,6 @@
|
||||
"errorMessageUnableToUnlinkStrava": "Unable to unlink Strava account",
|
||||
"modalUnlinkStravaTitle": "Unlink Strava",
|
||||
"modalUnlinkStravaBody": "Are you sure you want to unlink your Strava account? Unlinking your Strava account will remove all your Strava activities and gear from Endurain.",
|
||||
"bulkImportIntegrationTitle": "Bulk import",
|
||||
"bulkImportIntegrationBody": "Bulk import activities from files stored in the activity_files/bulk_import folder",
|
||||
"buttonBulkImport": "Import",
|
||||
"errorMessageUnableToImportActivities": "An error occurred while importing activities",
|
||||
"loadingMessageBulkImport": "Importing activities",
|
||||
"garminConnectIntegrationTitle": "Garmin Connect",
|
||||
"garminConnectIntegrationBody": "Garmin Connect is a health and fitness activity platform for users of Garmin devices",
|
||||
"loadingMessageRetrievingGarminConnectActivities": "Retrieving Garmin Connect activities",
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"myProfileSection": "My Profile",
|
||||
"myGoals": "My Goals",
|
||||
"securitySection": "Security",
|
||||
"integrationsSection": "Integrations"
|
||||
"integrationsSection": "Integrations",
|
||||
"importSection": "Import"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchGetRequest, fetchPutRequest, fetchDeleteRequest } from '@/utils/serviceUtils'
|
||||
import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest } from '@/utils/serviceUtils'
|
||||
|
||||
export const strava = {
|
||||
setUniqueUserStateStravaLink(state) {
|
||||
@@ -21,6 +21,9 @@ export const strava = {
|
||||
// Redirect to the Strava authorization URL
|
||||
window.location.href = stravaAuthUrl
|
||||
},
|
||||
importBikes() {
|
||||
return fetchPostRequest('strava/import/bikes');
|
||||
},
|
||||
linkStravaCallback(state, code, scope) {
|
||||
return fetchPutRequest(`strava/link?state=${state}&code=${code}&scope=${scope}`)
|
||||
},
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
|
||||
<!-- Include the SettingsUserGoals -->
|
||||
<SettingsUserGoals v-if="activeSection === 'myGoals'" />
|
||||
|
||||
<!-- Include the SettingsImportZone -->
|
||||
<SettingsImportZone v-if="activeSection === 'import'" />
|
||||
|
||||
</div>
|
||||
<!-- back button -->
|
||||
<BackButtonComponent />
|
||||
@@ -54,6 +58,7 @@ import SettingsUserProfileZone from '../components/Settings/SettingsUserProfileZ
|
||||
import SettingsSecurityZone from '../components/Settings/SettingsSecurityZone.vue'
|
||||
import SettingsIntegrationsZone from '../components/Settings/SettingsIntegrationsZone.vue'
|
||||
import BackButtonComponent from '@/components/GeneralComponents/BackButtonComponent.vue'
|
||||
import SettingsImportZone from '../components/Settings/SettingsImportZone.vue'
|
||||
import SettingsUserGoals from '../components/Settings/SettingsUserGoals.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
Reference in New Issue
Block a user