Merge branch 'master' into 0.16.0

This commit is contained in:
João Vitória Silva
2025-10-31 16:33:08 +00:00
11 changed files with 50 additions and 24 deletions

View File

@@ -86,6 +86,7 @@ ACTIVITY_ID_TO_NAME = {
37: "Ice Skate", 37: "Ice Skate",
38: "Soccer", 38: "Soccer",
39: "Padel", 39: "Padel",
40: "Treadmill",
# Add other mappings as needed based on the full list in define_activity_type comments if required # Add other mappings as needed based on the full list in define_activity_type comments if required
# "AlpineSki", # "AlpineSki",
# "BackcountrySki", # "BackcountrySki",
@@ -211,6 +212,7 @@ ACTIVITY_NAME_TO_ID.update(
"padel": 39, "padel": 39,
"padelball": 39, "padelball": 39,
"paddelball": 39, "paddelball": 39,
"treadmill": 40,
} }
) )
@@ -797,7 +799,7 @@ def calculate_activity_distances(activities: list[activities_schema.Activity]):
if activities is not None: if activities is not None:
# Calculate the distances # Calculate the distances
for activity in activities: for activity in activities:
if activity.activity_type in [1, 2, 3, 34]: if activity.activity_type in [1, 2, 3, 34, 40]:
run += activity.distance run += activity.distance
elif activity.activity_type in [4, 5, 6, 7, 27, 28, 29, 35, 36]: elif activity.activity_type in [4, 5, 6, 7, 27, 28, 29, 35, 36]:
bike += activity.distance bike += activity.distance

View File

@@ -406,7 +406,7 @@ class ExportService:
""" """
chunk_buffer = [] chunk_buffer = []
file_counter = 0 file_counter = 0
max_items_per_file = 500 max_items_per_file = batch_size
total_items = 0 total_items = 0
# Collect component data in batches # Collect component data in batches

View File

@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from safeuploads import FileValidator from safeuploads import FileValidator, FileSecurityConfig, SecurityLimits
from safeuploads.exceptions import FileValidationError from safeuploads.exceptions import FileValidationError
import users.user.schema as users_schema import users.user.schema as users_schema
@@ -39,8 +39,15 @@ import websocket.schema as websocket_schema
# Define the API router # Define the API router
router = APIRouter() router = APIRouter()
custom_limits = SecurityLimits(
max_uncompressed_size=2 * 1024 * 1024 * 1024,
max_number_files_same_type=2000,
)
custom_config = FileSecurityConfig()
custom_config.limits = custom_limits
# Initialize the file validator # Initialize the file validator
file_validator = FileValidator() file_validator = FileValidator(config=custom_config)
@router.get("", response_model=users_schema.UserMe) @router.get("", response_model=users_schema.UserMe)

View File

@@ -40,8 +40,7 @@ def calculate_user_goals(
return None return None
return [ return [
calculate_goal_progress_by_activity_type(goal, date, db) calculate_goal_progress_by_activity_type(goal, date, db) for goal in goals
for goal in goals
] ]
except HTTPException as http_err: except HTTPException as http_err:
raise http_err raise http_err
@@ -76,19 +75,19 @@ def calculate_goal_progress_by_activity_type(
user_goals_schema.UserGoalProgress | None: An object containing progress details for the goal, or None if no activities are found. user_goals_schema.UserGoalProgress | None: An object containing progress details for the goal, or None if no activities are found.
Raises: Raises:
HTTPException: If an error occurs during processing or database access. HTTPException: If an error occurs during processing or database access.
""" """
try: try:
start_date, end_date = get_start_end_date_by_interval(goal.interval, date) start_date, end_date = get_start_end_date_by_interval(goal.interval, date)
# Define activity type mappings # Define activity type mappings
TYPE_MAP = { TYPE_MAP = {
user_goals_schema.ActivityType.RUN: [1, 2, 3, 34], user_goals_schema.ActivityType.RUN: [1, 2, 3, 34, 40],
user_goals_schema.ActivityType.BIKE: [4, 5, 6, 7, 27, 28, 29, 35, 36], user_goals_schema.ActivityType.BIKE: [4, 5, 6, 7, 27, 28, 29, 35, 36],
user_goals_schema.ActivityType.SWIM: [8, 9], user_goals_schema.ActivityType.SWIM: [8, 9],
user_goals_schema.ActivityType.WALK: [11, 12], user_goals_schema.ActivityType.WALK: [11, 12],
} }
DEFAULT_TYPES = (19, 20) DEFAULT_TYPES = (19, 20)
# Get activity types based on goal.activity_type, default to [10, 19, 20] # Get activity types based on goal.activity_type, default to [10, 19, 20]
activity_types = TYPE_MAP.get(goal.activity_type, DEFAULT_TYPES) activity_types = TYPE_MAP.get(goal.activity_type, DEFAULT_TYPES)
@@ -96,7 +95,7 @@ def calculate_goal_progress_by_activity_type(
activities = activity_crud.get_user_activities_per_timeframe_and_activity_types( activities = activity_crud.get_user_activities_per_timeframe_and_activity_types(
goal.user_id, activity_types, start_date, end_date, db, True goal.user_id, activity_types, start_date, end_date, db, True
) )
# Calculate totals based on goal type # Calculate totals based on goal type
percentage_completed = 0 percentage_completed = 0
total_calories = 0 total_calories = 0
@@ -113,14 +112,20 @@ def calculate_goal_progress_by_activity_type(
total_distance = sum(activity.distance or 0 for activity in activities) total_distance = sum(activity.distance or 0 for activity in activities)
percentage_completed = (total_distance / goal.goal_distance) * 100 percentage_completed = (total_distance / goal.goal_distance) * 100
elif goal.goal_type == user_goals_schema.GoalType.ELEVATION: elif goal.goal_type == user_goals_schema.GoalType.ELEVATION:
total_elevation = sum(activity.elevation_gain or 0 for activity in activities) total_elevation = sum(
activity.elevation_gain or 0 for activity in activities
)
percentage_completed = (total_elevation / goal.goal_elevation) * 100 percentage_completed = (total_elevation / goal.goal_elevation) * 100
elif goal.goal_type == user_goals_schema.GoalType.DURATION: elif goal.goal_type == user_goals_schema.GoalType.DURATION:
total_duration = sum(activity.total_elapsed_time or 0 for activity in activities) total_duration = sum(
activity.total_elapsed_time or 0 for activity in activities
)
percentage_completed = (total_duration / goal.goal_duration) * 100 percentage_completed = (total_duration / goal.goal_duration) * 100
elif goal.goal_type == user_goals_schema.GoalType.ACTIVITIES: elif goal.goal_type == user_goals_schema.GoalType.ACTIVITIES:
total_activities_number = len(activities) total_activities_number = len(activities)
percentage_completed = (total_activities_number / goal.goal_activities_number) * 100 percentage_completed = (
total_activities_number / goal.goal_activities_number
) * 100
if percentage_completed > 100: if percentage_completed > 100:
percentage_completed = 100 percentage_completed = 100
@@ -208,7 +213,9 @@ def get_start_end_date_by_interval(
""" """
date_obj = datetime.strptime(date, "%Y-%m-%d") date_obj = datetime.strptime(date, "%Y-%m-%d")
if interval == "yearly": if interval == "yearly":
start_date = date_obj.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) start_date = date_obj.replace(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0
)
# Calculate the last second of December 31st of the same year # Calculate the last second of December 31st of the same year
end_date = datetime(date_obj.year, 12, 31, 23, 59, 59) end_date = datetime(date_obj.year, 12, 31, 23, 59, 59)
elif interval == "weekly": elif interval == "weekly":

View File

@@ -245,6 +245,7 @@ The table bellow details the activity types supported by Endurain.
| Run | 1 | | Run | 1 |
| Trail run | 2 | | Trail run | 2 |
| Track run | 34 | | Track run | 34 |
| Treadmill run | 40 |
| Virtual run | 3 | | Virtual run | 3 |
| Road cycling | 4 | | Road cycling | 4 |
| Gravel cycling | 5 | | Gravel cycling | 5 |

View File

@@ -10245,4 +10245,4 @@
} }
} }
} }
} }

View File

@@ -81,6 +81,9 @@
<option value="34"> <option value="34">
{{ $t('editActivityModalComponent.modalEditActivityTypeOption34') }} {{ $t('editActivityModalComponent.modalEditActivityTypeOption34') }}
</option> </option>
<option value="40">
{{ $t('editActivityModalComponent.modalEditActivityTypeOption40') }}
</option>
<option value="3"> <option value="3">
{{ $t('editActivityModalComponent.modalEditActivityTypeOption3') }} {{ $t('editActivityModalComponent.modalEditActivityTypeOption3') }}
</option> </option>

View File

@@ -38,5 +38,6 @@
"iceSkate": "Ice skate", "iceSkate": "Ice skate",
"soccer": "Soccer", "soccer": "Soccer",
"padel": "Padel", "padel": "Padel",
"treadmillRun": "Treadmill run",
"labelWorkout": " workout" "labelWorkout": " workout"
} }

View File

@@ -46,6 +46,7 @@
"modalEditActivityTypeOption37": "Ice skate", "modalEditActivityTypeOption37": "Ice skate",
"modalEditActivityTypeOption38": "Soccer", "modalEditActivityTypeOption38": "Soccer",
"modalEditActivityTypeOption39": "Padel", "modalEditActivityTypeOption39": "Padel",
"modalEditActivityTypeOption40": "Treadmill run",
"modalEditActivityVisibilityLabel": "Visibility", "modalEditActivityVisibilityLabel": "Visibility",
"modalEditActivityVisibilityOption0": "Public", "modalEditActivityVisibilityOption0": "Public",
"modalEditActivityVisibilityOption1": "Followers", "modalEditActivityVisibilityOption1": "Followers",
@@ -65,4 +66,4 @@
"modalEditActivityHideGearLabel": "Hide gear", "modalEditActivityHideGearLabel": "Hide gear",
"successActivityEdit": "Activity edited successfully", "successActivityEdit": "Activity edited successfully",
"errorActivityEdit": "Error editing activity" "errorActivityEdit": "Error editing activity"
} }

View File

@@ -8,7 +8,7 @@ import { formatDateMed, formatTime, formatSecondsToMinutes } from '@/utils/dateT
*/ */
const ACTIVITY_TYPES = [ const ACTIVITY_TYPES = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
] ]
/** /**
@@ -60,7 +60,8 @@ const activityLabelMap = {
36: (t) => t('activityItems.ebikeMountainRide'), 36: (t) => t('activityItems.ebikeMountainRide'),
37: (t) => t('activityItems.iceSkate'), 37: (t) => t('activityItems.iceSkate'),
38: (t) => t('activityItems.soccer'), 38: (t) => t('activityItems.soccer'),
39: (t) => t('activityItems.padel') 39: (t) => t('activityItems.padel'),
40: (t) => t('activityItems.treadmillRun')
} }
/** /**
@@ -282,7 +283,8 @@ export function activityTypeIsRunning(activity) {
activity.activity_type === 1 || activity.activity_type === 1 ||
activity.activity_type === 2 || activity.activity_type === 2 ||
activity.activity_type === 3 || activity.activity_type === 3 ||
activity.activity_type === 34 activity.activity_type === 34 ||
activity.activity_type === 40
) )
} }
/** /**
@@ -290,14 +292,15 @@ export function activityTypeIsRunning(activity) {
* *
* @param {Object} activity - The activity object to check. * @param {Object} activity - The activity object to check.
* @param {number} activity.activity_type - The type identifier of the activity. * @param {number} activity.activity_type - The type identifier of the activity.
* @returns {boolean} Returns true if the activity is not running-related (types 1,2,3 or 34), otherwise false. * @returns {boolean} Returns true if the activity is not running-related (types 1,2,3,34 and 40), otherwise false.
*/ */
export function activityTypeNotRunning(activity) { export function activityTypeNotRunning(activity) {
return ( return (
activity.activity_type !== 1 && activity.activity_type !== 1 &&
activity.activity_type !== 2 && activity.activity_type !== 2 &&
activity.activity_type !== 3 && activity.activity_type !== 3 &&
activity.activity_type !== 34 activity.activity_type !== 34 &&
activity.activity_type !== 40
) )
} }
@@ -710,7 +713,8 @@ export function getIcon(typeId) {
36: ['fas', 'person-biking'], 36: ['fas', 'person-biking'],
37: ['fas', 'person-skating'], 37: ['fas', 'person-skating'],
38: ['fas', 'futbol'], 38: ['fas', 'futbol'],
39: ['fas', 'table-tennis-paddle-ball'] 39: ['fas', 'table-tennis-paddle-ball'],
40: ['fas', 'person-running'], // Treadmill run icon might be better if available
} }
return iconMap[typeId] || ['fas', 'dumbbell'] return iconMap[typeId] || ['fas', 'dumbbell']

View File

@@ -258,7 +258,7 @@ const fetchGearResults = debounce(async (query) => {
function updateSearchResultsBasedOnActivityType() { function updateSearchResultsBasedOnActivityType() {
if (searchSelectActivityType.value === '1') { if (searchSelectActivityType.value === '1') {
searchResults.value = searchResultsOriginal.value.filter((user) => searchResults.value = searchResultsOriginal.value.filter((user) =>
[1, 2, 3, 34].includes(user.activity_type) [1, 2, 3, 34, 40].includes(user.activity_type)
) )
} else if (searchSelectActivityType.value === '2') { } else if (searchSelectActivityType.value === '2') {
searchResults.value = searchResultsOriginal.value.filter((user) => searchResults.value = searchResultsOriginal.value.filter((user) =>