Merge branch 'pr/390' into 0.16.0

This commit is contained in:
João Vitória Silva
2025-12-03 21:55:53 +00:00
11 changed files with 93 additions and 6 deletions

View File

@@ -383,14 +383,21 @@ def transform_activity_streams_hr(activity_stream, activity, db):
# Get the user details to calculate heart rate zones
detail_user = users_crud.get_user_by_id(activity.user_id, db)
if not detail_user or not detail_user.birthdate:
# If user details are not available or birthdate is missing, return the activity stream as is
if not detail_user:
# If user details are not available, return the activity stream as is
return activity_stream
# Calculate the maximum heart rate based on the user's birthdate
year = int(detail_user.birthdate.split("-")[0])
current_year = datetime.datetime.now().year
max_heart_rate = 220 - (current_year - year)
# Use user's max_heart_rate if set, otherwise calculate based on age formula
if detail_user.max_heart_rate:
max_heart_rate = detail_user.max_heart_rate
elif detail_user.birthdate:
# Calculate the maximum heart rate based on the user's birthdate
year = int(detail_user.birthdate.split("-")[0])
current_year = datetime.datetime.now().year
max_heart_rate = 220 - (current_year - year)
else:
# If neither max_heart_rate nor birthdate is available, return the activity stream as is
return activity_stream
# Calculate heart rate zones based on the maximum heart rate
zone_1 = max_heart_rate * 0.6

View File

@@ -700,6 +700,17 @@ def upgrade() -> None:
op.f("ix_health_sleep_user_id"), "health_sleep", ["user_id"], unique=False
)
# Add max_heart_rate column to users table
op.add_column(
"users",
sa.Column(
"max_heart_rate",
sa.Integer(),
nullable=True,
comment="User maximum heart rate (bpm)",
),
)
# Update health_weight source column to "garmin" where not null
connection.execute(
sa.text(
@@ -715,6 +726,9 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Remove max_heart_rate column from users table
op.drop_column("users", "max_heart_rate")
# Drop table health_sleep
op.drop_index(op.f("ix_health_sleep_user_id"), table_name="health_sleep")
op.drop_index(op.f("ix_health_sleep_date"), table_name="health_sleep")

View File

@@ -98,6 +98,9 @@ class User(Base):
comment="User units (one digit)(1 - metric, 2 - imperial)",
)
height = Column(Integer, nullable=True, comment="User height in centimeters")
max_heart_rate = Column(
Integer, nullable=True, comment="User maximum heart rate (bpm)"
)
access_type = Column(
Integer, nullable=False, comment="User type (one digit)(1 - user, 2 - admin)"
)

View File

@@ -132,6 +132,7 @@ class UserBase(BaseModel):
gender: Gender = Gender.MALE
units: server_settings_schema.Units = server_settings_schema.Units.METRIC
height: int | None = None
max_heart_rate: int | None = None
first_day_of_week: WeekDay = WeekDay.MONDAY
currency: server_settings_schema.Currency = server_settings_schema.Currency.EURO

View File

@@ -125,6 +125,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1762,6 +1763,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -1805,6 +1807,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2454,6 +2457,7 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -2784,6 +2788,7 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -3376,6 +3381,7 @@
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.0",
"@typescript-eslint/types": "8.48.0",
@@ -4012,6 +4018,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4348,6 +4355,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -4482,6 +4490,7 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -5159,6 +5168,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5219,6 +5229,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -5266,6 +5277,7 @@
"integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"natural-compare": "^1.4.0",
@@ -7553,6 +7565,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -7857,6 +7870,7 @@
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -8742,6 +8756,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -8941,6 +8956,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9131,6 +9147,7 @@
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -9255,6 +9272,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -9365,6 +9383,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",
@@ -9394,6 +9413,7 @@
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.0",
"eslint-scope": "^8.2.0",
@@ -9848,6 +9868,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -9975,6 +9996,7 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},

View File

@@ -142,6 +142,15 @@
</span>
<span v-else>{{ $t('generalItems.labelNotApplicable') }}</span>
</p>
<!-- user max heart rate -->
<p>
<font-awesome-icon :icon="['fas', 'heart-pulse']" class="me-2" />
<b>{{ $t('settingsUserProfileZone.maxHeartRateLabel') }}: </b>
<span v-if="authStore.user.max_heart_rate"
>{{ authStore.user.max_heart_rate }} {{ $t('generalItems.unitsBpm') }}</span
>
<span v-else>{{ $t('generalItems.labelNotApplicable') }}</span>
</p>
<!-- user preferred language -->
<p>
<font-awesome-icon :icon="['fas', 'language']" class="me-2" />

View File

@@ -310,6 +310,27 @@
</div>
</div>
</div>
<!-- max heart rate fields -->
<label for="userMaxHeartRateAddEdit"
><b>{{
$t('usersAddEditUserModalComponent.addEditUserModalMaxHeartRateLabel')
}}</b></label
>
<div class="input-group">
<input
class="form-control"
type="number"
name="userMaxHeartRateAddEdit"
:placeholder="
$t('usersAddEditUserModalComponent.addEditUserModalMaxHeartRatePlaceholder')
"
v-model="newEditUserMaxHeartRate"
min="100"
max="250"
step="1"
/>
<span class="input-group-text">{{ $t('generalItems.unitsBpm') }}</span>
</div>
<!-- preferred language fields -->
<label for="userPreferredLanguageAddEdit"
><b
@@ -499,6 +520,7 @@ const newEditUserCurrency = ref(serverSettingsStore.serverSettings.currency)
const newEditUserHeightCms = ref(null)
const newEditUserHeightFeet = ref(null)
const newEditUserHeightInches = ref(null)
const newEditUserMaxHeartRate = ref(null)
const newEditUserFirstDayOfWeek = ref(1)
const isFeetValid = computed(
() => newEditUserHeightFeet.value >= 0 && newEditUserHeightFeet.value <= 10
@@ -540,6 +562,7 @@ if (props.user) {
newEditUserUnits.value = props.user.units
newEditUserCurrency.value = props.user.currency
newEditUserHeightCms.value = props.user.height
newEditUserMaxHeartRate.value = props.user.max_heart_rate
newEditUserPreferredLanguage.value = props.user.preferred_language
newEditUserFirstDayOfWeek.value = props.user.first_day_of_week
newEditUserAccessType.value = props.user.access_type
@@ -648,6 +671,7 @@ async function submitAddUserForm() {
units: newEditUserUnits.value,
currency: newEditUserCurrency.value,
height: newEditUserHeightCms.value,
max_heart_rate: newEditUserMaxHeartRate.value,
access_type: newEditUserAccessType.value,
photo_path: null,
first_day_of_week: newEditUserFirstDayOfWeek.value,
@@ -691,6 +715,7 @@ async function submitEditUserForm() {
units: newEditUserUnits.value,
currency: newEditUserCurrency.value,
height: newEditUserHeightCms.value,
max_heart_rate: newEditUserMaxHeartRate.value,
preferred_language: newEditUserPreferredLanguage.value,
first_day_of_week: newEditUserFirstDayOfWeek.value,
access_type: newEditUserAccessType.value,

View File

@@ -16,6 +16,7 @@
"unitsOption2": "Imperial",
"currencyLabel": "Währung",
"heightLabel": "Höhe",
"maxHeartRateLabel": "Maximale Herzfrequenz",
"preferredLanguageLabel": "Bevorzugte Sprache",
"firstDayOfWeekLabel": "Erster Tag der Woche",
"accessTypeLabel": "Zugangsart",

View File

@@ -26,6 +26,8 @@
"addEditUserModalCurrencyLabel": "Währung",
"addEditUserModalHeightLabel": "Größe",
"addEditUserModalHeightPlaceholder": "Größe",
"addEditUserModalMaxHeartRateLabel": "Maximale Herzfrequenz (optional)",
"addEditUserModalMaxHeartRatePlaceholder": "Maximale Herzfrequenz",
"addEditUserModalFeetValidationLabel": "Ungültige Höhe. Bitte geben Sie eine gültige Höhe in Fuß ein.",
"addEditUserModalInchesValidationLabel": "Ungültige Höhe. Bitte geben Sie eine gültige Höhe in Zoll ein.",
"addEditUserModalUserPreferredLanguageLabel": "Bevorzugte Sprache",

View File

@@ -16,6 +16,7 @@
"unitsOption2": "Imperial",
"currencyLabel": "Currency",
"heightLabel": "Height",
"maxHeartRateLabel": "Max heart rate",
"preferredLanguageLabel": "Preferred language",
"firstDayOfWeekLabel": "First day of week",
"accessTypeLabel": "Access type",

View File

@@ -26,6 +26,8 @@
"addEditUserModalCurrencyLabel": "Currency",
"addEditUserModalHeightLabel": "Height",
"addEditUserModalHeightPlaceholder": "Height",
"addEditUserModalMaxHeartRateLabel": "Max heart rate (optional)",
"addEditUserModalMaxHeartRatePlaceholder": "Max heart rate",
"addEditUserModalFeetValidationLabel": "Invalid height. Please enter a valid height in feet.",
"addEditUserModalInchesValidationLabel": "Invalid height. Please enter a valid height in inches.",
"addEditUserModalUserPreferredLanguageLabel": "Preferred language",