Skip to content

Commit

Permalink
Measurements (#305)
Browse files Browse the repository at this point in the history
* build(backend): update deps

* feat(backend): use new database fn

I was the one who implemented this: SeaQL/sea-query#671!

* feat(backend): config param for measurements

* feat(frontend): allow toggling measurements

* try(backend): checking sea orm PR

* Revert "try(backend): checking sea orm PR"

This reverts commit f9d602c.

* feat(*): resolver to get user to exercise details

* chore(graphql): add user exercise details query

* feat(backend): increase worker count for application jobs

* feat(frontend): add link to exercise details page

* feat(frontend): apply typography styles

* feat(frontend): display exercise instructions

* fix(frontend): handle mobile layout for instructions

* refactor(backend): change name of fn

* feat(backend): return history for exercise

* feat(frontend): display small part of history

* refactor(frontend): change component name

* refactor(backend): calculate using prefs

* fix(frontend): import path

* feat(backend): take user unit prefs into account

* refactor(backend): change name of fn

* Revert "feat(backend): take user unit prefs into account"

This reverts commit c617a19.

* feat(frontend): handle units before commit

* fix(backend): append history to start of workout

* feat(*): store workout unit in db

* try(backend): unitless measurements

* Revert "try(backend): unitless measurements"

This reverts commit de0d3fc.

* fix(backend): change number of reps to `usize`

* refactor(frontend): change name of property

* feat(backend): store unit system in backend

* feat(*): remove individual refs to separate weight and distance

* docs(backend): add info to fn

* feat(frontend): send calcs in correct format

* feat(*): calculate metrics on the backend

* fix(backend): conversions not being returned

* try(backend): did something

* Revert "try(backend): did something"

This reverts commit 9e467ab.

* fix(frontend): remove useless imports

* refactor(backend): add required attributes

try(backend): change select type

feat(backend): use new derive macro

* Revert "refactor(backend): add required attributes"

This reverts commit 8dae1ec.

* feat(frontend): display lifetime stats

* feat(graphql): get lot for set

* feat(frontend): display stats in history

* chore(*): remove user preferences

* chore(backend): add fixme comments

* chore(frontend): remove select box for unit system

* feat(frontend): display more stat

* build(backend): bump version

* build(frontend): update deps

* build(docs): update deps

* fix(docs): use correct path

* chore(docs): make info clearer

* feat(backend): allow updating exercises
  • Loading branch information
IgnisDa authored Aug 28, 2023
1 parent 923e5b1 commit 2376f9f
Show file tree
Hide file tree
Showing 39 changed files with 1,447 additions and 707 deletions.
243 changes: 115 additions & 128 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions apps/backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ryot"
version = "2.9.4"
version = "2.9.5"
edition = "2021"
repository = "https://github.com/IgnisDa/ryot"
license = "GPL-V3"
Expand All @@ -17,7 +17,7 @@ async-graphql = { version = "6.0.4", features = [
] }
async-graphql-axum = "6.0.4"
async-trait = "0.1.73"
aws-sdk-s3 = "0.29.0"
aws-sdk-s3 = "0.30.0"
axum = { version = "0.6.20", features = ["macros", "multipart"] }
chrono = "0.4.26"
convert_case = "0.6.0"
Expand All @@ -44,9 +44,9 @@ quick-xml = { version = "0.30.0", features = ["serde", "serialize"] }
retainer = "0.3.0"
rs-utils = { path = "../../libs/rs-utils" }
rust-embed = "6.8.1"
rust_decimal = "1.31.0"
rust_decimal_macros = "1.31.0"
schematic = { version = "0.11.2", features = [
rust_decimal = "1.32.0"
rust_decimal_macros = "1.32.0"
schematic = { version = "0.11.4", features = [
"schema",
"toml",
"typescript",
Expand All @@ -66,9 +66,9 @@ sea-orm = { version = "0.12.2", features = [
"with-rust_decimal",
] }
sea-orm-migration = "0.12.2"
sea-query = "0.30.0"
sea-query = "0.30.1"
semver = "1.0.18"
serde = { version = "1.0.185", features = ["derive"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.105"
serde_with = { version = "3.3.0", features = ["chrono_0_4"] }
slug = "0.1.4"
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/entities/user_to_exercise.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1

use async_graphql::SimpleObject;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

use crate::models::fitness::UserToExerciseExtraInformation;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "user_to_exercise")]
pub struct Model {
#[graphql(skip)]
#[sea_orm(primary_key, auto_increment = false)]
pub user_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
Expand Down
39 changes: 33 additions & 6 deletions apps/backend/src/fitness/exercise/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::{anyhow, Result};
use async_graphql::{InputObject, SimpleObject};
use chrono::Utc;
use rs_utils::LengthVec;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use rust_decimal_macros::dec;
use sea_orm::{
prelude::DateTimeUtc, ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection,
Expand All @@ -22,6 +23,7 @@ use crate::{
UserToExerciseBestSetExtraInformation, UserToExerciseExtraInformation,
UserToExerciseHistoryExtraInformation, WorkoutSetPersonalBest, WorkoutSetRecord,
},
users::{UserExercisePreferences, UserUnitSystem},
};

fn get_best_set_index(records: &[WorkoutSetRecord]) -> Option<usize> {
Expand All @@ -31,7 +33,11 @@ fn get_best_set_index(records: &[WorkoutSetRecord]) -> Option<usize> {
.max_by_key(|(_, record)| {
record.statistic.duration.unwrap_or(dec!(0))
+ record.statistic.distance.unwrap_or(dec!(0))
+ record.statistic.reps.unwrap_or(dec!(0))
+ record
.statistic
.reps
.map(|r| Decimal::from_usize(r).unwrap())
.unwrap_or(dec!(0))
+ record.statistic.weight.unwrap_or(dec!(0))
})
.map(|(index, _)| index)
Expand Down Expand Up @@ -102,6 +108,24 @@ pub struct UserWorkoutSetRecord {
pub lot: SetLot,
}

impl UserWorkoutSetRecord {
pub fn translate_units(self, unit_type: UserUnitSystem) -> Self {
let mut du = self;
match unit_type {
UserUnitSystem::Metric => du,
UserUnitSystem::Imperial => {
if let Some(w) = du.statistic.weight.as_mut() {
*w = *w * dec!(0.45359);
}
if let Some(d) = du.statistic.distance.as_mut() {
*d = *d * dec!(1.60934);
}
du
}
}
}
}

#[derive(Clone, Debug, Deserialize, Serialize, InputObject)]
pub struct UserExerciseInput {
pub exercise_id: i32,
Expand All @@ -127,7 +151,7 @@ impl UserWorkoutInput {
user_id: i32,
db: &DatabaseConnection,
id: String,
save_history: usize,
preferences: UserExercisePreferences,
) -> Result<String> {
let mut exercises = vec![];
let mut workout_totals = vec![];
Expand Down Expand Up @@ -167,7 +191,7 @@ impl UserWorkoutInput {
Some(e) => {
let performed = e.num_times_performed;
let mut extra_info = e.extra_information.clone();
extra_info.history.push(history_item);
extra_info.history.insert(0, history_item);
let mut up: user_to_exercise::ActiveModel = e.into();
up.num_times_performed = ActiveValue::Set(performed + 1);
up.extra_information = ActiveValue::Set(extra_info);
Expand All @@ -176,10 +200,11 @@ impl UserWorkoutInput {
}
};
for set in ex.sets {
let set = set.clone().translate_units(preferences.unit_system);
if let Some(r) = set.statistic.reps {
total.reps += r;
if let Some(w) = set.statistic.weight {
total.weight += w * r;
total.weight += w * Decimal::from_usize(r).unwrap();
}
}
if let Some(d) = set.statistic.duration {
Expand Down Expand Up @@ -230,8 +255,10 @@ impl UserWorkoutInput {
data: set.clone(),
};
if let Some(record) = personal_bests.iter_mut().find(|pb| pb.lot == *best) {
let mut data =
LengthVec::from_vec_and_length(record.sets.clone(), save_history);
let mut data = LengthVec::from_vec_and_length(
record.sets.clone(),
preferences.save_history,
);
data.push_front(to_insert_record);
record.sets = data.into_vec();
} else {
Expand Down
136 changes: 104 additions & 32 deletions apps/backend/src/fitness/exercise/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use crate::{
config::AppConfig,
entities::{
exercise,
prelude::{Exercise, UserMeasurement},
user_measurement,
prelude::{Exercise, UserMeasurement, UserToExercise, Workout},
user_measurement, user_to_exercise, workout,
},
file_storage::FileStorageService,
migrator::{
Expand All @@ -28,7 +28,7 @@ use crate::{
models::{
fitness::{
Exercise as GithubExercise, ExerciseAttributes, ExerciseCategory, ExerciseMuscles,
GithubExerciseAttributes,
GithubExerciseAttributes, WorkoutSetRecord,
},
SearchDetails, SearchResults, StoredUrl,
},
Expand Down Expand Up @@ -62,7 +62,7 @@ struct ExercisesListInput {
}

#[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
struct ExerciseInformation {
struct ExerciseParameters {
/// All filters applicable to an exercises query.
filters: ExerciseFilters,
download_required: bool,
Expand All @@ -85,15 +85,29 @@ struct UserMeasurementsListInput {
end_time: Option<DateTimeUtc>,
}

#[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
struct UserExerciseHistoryInformation {
workout_id: String,
workout_name: Option<String>,
workout_time: DateTimeUtc,
sets: Vec<WorkoutSetRecord>,
}

#[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
struct UserExerciseInformation {
details: user_to_exercise::Model,
history: Vec<UserExerciseHistoryInformation>,
}

#[derive(Default)]
pub struct ExerciseQuery;

#[Object]
impl ExerciseQuery {
/// Get all the information related to exercises.
async fn exercise_information(&self, gql_ctx: &Context<'_>) -> Result<ExerciseInformation> {
/// Get all the parameters related to exercises.
async fn exercise_parameters(&self, gql_ctx: &Context<'_>) -> Result<ExerciseParameters> {
let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
service.exercise_information().await
service.exercise_parameters().await
}

/// Get a paginated list of exercises in the database.
Expand All @@ -106,10 +120,25 @@ impl ExerciseQuery {
service.exercises_list(input).await
}

/// Get information about an exercise.
async fn exercise(&self, gql_ctx: &Context<'_>, exercise_id: i32) -> Result<exercise::Model> {
/// Get details about an exercise.
async fn exercise_details(
&self,
gql_ctx: &Context<'_>,
exercise_id: i32,
) -> Result<exercise::Model> {
let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
service.exercise_details(exercise_id).await
}

/// Get information about an exercise for a user.
async fn user_exercise_details(
&self,
gql_ctx: &Context<'_>,
exercise_id: i32,
) -> Result<Option<UserExerciseInformation>> {
let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
service.exercise(exercise_id).await
let user_id = service.user_id_from_ctx(gql_ctx).await?;
service.user_exercise_details(exercise_id, user_id).await
}

/// Get all the measurements for a user.
Expand Down Expand Up @@ -202,9 +231,9 @@ impl ExerciseService {
}

impl ExerciseService {
async fn exercise_information(&self) -> Result<ExerciseInformation> {
async fn exercise_parameters(&self) -> Result<ExerciseParameters> {
let download_required = Exercise::find().count(&self.db).await? == 0;
Ok(ExerciseInformation {
Ok(ExerciseParameters {
filters: ExerciseFilters {
lot: ExerciseLot::iter().collect_vec(),
level: ExerciseLevel::iter().collect_vec(),
Expand Down Expand Up @@ -242,14 +271,58 @@ impl ExerciseService {
.collect())
}

async fn exercise(&self, exercise_id: i32) -> Result<exercise::Model> {
async fn exercise_details(&self, exercise_id: i32) -> Result<exercise::Model> {
let maybe_exercise = Exercise::find_by_id(exercise_id).one(&self.db).await?;
match maybe_exercise {
None => Err(Error::new("Exercise with the given ID could not be found.")),
Some(e) => Ok(e.graphql_repr(&self.file_storage_service).await),
}
}

async fn user_exercise_details(
&self,
exercise_id: i32,
user_id: i32,
) -> Result<Option<UserExerciseInformation>> {
if let Some(details) = UserToExercise::find_by_id((user_id, exercise_id))
.one(&self.db)
.await?
{
let workouts = Workout::find()
.filter(
workout::Column::Id.is_in(
details
.extra_information
.history
.iter()
.map(|h| h.workout_id.clone()),
),
)
.all(&self.db)
.await?;
let history = workouts
.into_iter()
.map(|w| {
let element = details
.extra_information
.history
.iter()
.find(|h| h.workout_id == w.id)
.unwrap();
UserExerciseHistoryInformation {
workout_id: w.id,
workout_name: w.name,
workout_time: w.start_time,
sets: w.information.exercises[element.idx].sets.clone(),
}
})
.collect();
Ok(Some(UserExerciseInformation { details, history }))
} else {
Ok(None)
}
}

async fn exercises_list(
&self,
input: ExercisesListInput,
Expand Down Expand Up @@ -326,12 +399,26 @@ impl ExerciseService {
}

pub async fn update_exercise(&self, ex: GithubExercise) -> Result<()> {
if Exercise::find()
let attributes = ExerciseAttributes {
muscles: vec![],
instructions: ex.attributes.instructions,
internal_images: ex
.attributes
.images
.into_iter()
.map(StoredUrl::Url)
.collect(),
images: vec![],
};
if let Some(e) = Exercise::find()
.filter(exercise::Column::Identifier.eq(&ex.identifier))
.one(&self.db)
.await?
.is_none()
{
let mut db_ex: exercise::ActiveModel = e.into();
db_ex.attributes = ActiveValue::Set(attributes);
db_ex.update(&self.db).await?;
} else {
let lot = match ex.attributes.category {
ExerciseCategory::Stretching => ExerciseLot::Duration,
ExerciseCategory::Plyometrics => ExerciseLot::Duration,
Expand All @@ -348,17 +435,7 @@ impl ExerciseService {
name: ActiveValue::Set(ex.name),
identifier: ActiveValue::Set(ex.identifier),
muscles: ActiveValue::Set(ExerciseMuscles(muscles)),
attributes: ActiveValue::Set(ExerciseAttributes {
muscles: vec![],
instructions: ex.attributes.instructions,
internal_images: ex
.attributes
.images
.into_iter()
.map(StoredUrl::Url)
.collect(),
images: vec![],
}),
attributes: ActiveValue::Set(attributes),
lot: ActiveValue::Set(lot),
level: ActiveValue::Set(ex.attributes.level),
force: ActiveValue::Set(ex.attributes.force),
Expand Down Expand Up @@ -429,12 +506,7 @@ impl ExerciseService {
let sf = Sonyflake::new().unwrap();
let id = sf.next_id().unwrap().to_string();
let identifier = input
.calculate_and_commit(
user_id,
&self.db,
id,
user.preferences.fitness.exercises.save_history,
)
.calculate_and_commit(user_id, &self.db, id, user.preferences.fitness.exercises)
.await?;
Ok(identifier)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/importer/media_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ pub async fn import(input: DeployMediaTrackerImportInput) -> Result<ImportResult
let data_len = data.len();

let mut final_data = vec![];
// TODO: Technically this can be done in parallel, by executing requests in
// DEV: Technically this can be done in parallel, by executing requests in
// batches. Example: https://users.rust-lang.org/t/can-tokio-semaphore-be-used-to-limit-spawned-tasks/59899.
for (idx, d) in data.into_iter().enumerate() {
let lot = MetadataLot::from(d.media_type.clone());
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ async fn main() -> Result<()> {
.build_fn(yank_integrations_data)
})
// application jobs
.register_with_count(1, move |c| {
.register_with_count(3, move |c| {
WorkerBuilder::new(format!("perform_application_job-{c}"))
.layer(ApalisTraceLayer::new())
.layer(ApalisRateLimitLayer::new(
Expand Down
Loading

0 comments on commit 2376f9f

Please sign in to comment.