Add treadmill sport type & bump version

This commit is contained in:
João Vitória Silva
2025-10-31 11:56:11 +00:00
parent 0341d87344
commit d3e412604b
13 changed files with 45 additions and 26 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

@@ -7,7 +7,7 @@ from cryptography.fernet import Fernet
import core.logger as core_logger import core.logger as core_logger
# Constant related to version # Constant related to version
API_VERSION = "v0.15.4" API_VERSION = "v0.15.5"
LICENSE_NAME = "GNU Affero General Public License v3.0 or later" LICENSE_NAME = "GNU Affero General Public License v3.0 or later"
LICENSE_IDENTIFIER = "AGPL-3.0-or-later" LICENSE_IDENTIFIER = "AGPL-3.0-or-later"
LICENSE_URL = "https://spdx.org/licenses/AGPL-3.0-or-later.html" LICENSE_URL = "https://spdx.org/licenses/AGPL-3.0-or-later.html"

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

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "endurain" name = "endurain"
version = "0.15.4" version = "0.15.5"
description = "Endurain API for the Endurain app" description = "Endurain API for the Endurain app"
authors = ["João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com>"] authors = ["João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com>"]
readme = "README.md" readme = "README.md"

View File

@@ -244,6 +244,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

@@ -1,12 +1,12 @@
{ {
"name": "endurain", "name": "endurain",
"version": "0.15.4", "version": "0.15.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "endurain", "name": "endurain",
"version": "0.15.4", "version": "0.15.5",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.1", "@fortawesome/fontawesome-svg-core": "^6.7.1",
"@fortawesome/free-brands-svg-icons": "^6.7.1", "@fortawesome/free-brands-svg-icons": "^6.7.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "endurain", "name": "endurain",
"version": "0.15.4", "version": "0.15.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

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

@@ -23,7 +23,7 @@
<a class="link-body-emphasis" href="https://discord.gg/6VUjUq2uZR" <a class="link-body-emphasis" href="https://discord.gg/6VUjUq2uZR"
><font-awesome-icon :icon="['fab', 'fa-discord']" ><font-awesome-icon :icon="['fab', 'fa-discord']"
/></a> /></a>
v0.15.4 v0.15.5
</p> </p>
<p class="text-center text-muted"> <p class="text-center text-muted">
<img <img

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) =>