Skip to content

Commit 9b7c801

Browse files
authored
Workout templates (#920)
* feat(frontend): change name of route * feat(backend): change path of user uploads * build(backend): add `mime_guess` deps * fix(backend): make correct put request from backend * ci: switch to using bun * ci: remove references to yarn * Revert "ci: switch to using bun" This reverts commit c2f991e. * Revert "ci: remove references to yarn" This reverts commit dd5bfa6. * feat(frontend): adapt to allow creating templates * fix(frontend): change dynamically * fix(backend): remove alias * feat(database): remove workout comment from top level * feat(backend): adapt to new database schema * chore(frontend): adapt to new gql schema * fix(frontend): duplicate comment as well * refactor(backend): generate default argon * fix(frontend): hide menu entry when creating template * refactor(backend): to account for new workout templates * fix(backend): skip correct input * chore(frontend): adapt to new gql schema * feat(*): use better types * fix(frontend): remove heading * feat(backend): make fields nullable * chore(frontend): adapt to new gql schema * fix(frontend): change name of btn * feat(frontend): add conditionals * chore(frontend): remove title * chore(frontend): change page title * fix(frontend): send workout create event only when it actually happens * refactor(frontend): use functions * feat(frontend): adapt to new templates * fix(frontend): add more clarifying text * feat(backend): make more fields nullanble * feat(*): change lot to be non nullable * fix(backend): do not serialize nulls * refactor(frontend): change name of module * feat(frontend): start adjusting for new endpoint * feat(frontend): duplicate workout accurately * feat(graphql): extract information into fragment * fix(frontend): invalidate query correctly * fix(frontend): extract type * feat(frontend): hide done on when not applicable * refactor(frontend): remove duplicated code * fix(frontend): show done on always * fix(frontend): no conditional * feat(frontend): enhance cookie for storing workout * refactor(frontend): fn to perform decision * fix(frontend): change word * fix(backend): remove useless extra line * fix(frontend): change text when disabled * feat(frontend): make workout list page dynamic * fix(frontend): display dynamic title * feat(frontend): make page more dynamic * feat(frontend): make page more accepting to changes * feat(frontend): make stuff plural * feat(frontend): redirect to correct page * fix(frontend): inline invariant * refactor(backend): change name of query * chore(frontend): adapt to new gql schema * fix(frontend): make plural * fix(frontend): more changes * feat(database): remove `is_demo` column from user * feat(backend): adapt to new gql schema * feat(frontend): adapt to new gql schema
1 parent 5aa3fd1 commit 9b7c801

34 files changed

+753
-525
lines changed

Cargo.lock

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/backend/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ lettre = { version = "=0.11.7", features = [
4949
"builder",
5050
], default-features = false }
5151
markdown = "=1.0.0-alpha.18"
52+
mime_guess = "=2.0.5"
5253
nanoid = { workspace = true }
5354
openidconnect = "=3.5.0"
5455
paginate = "=1.1.11"

apps/backend/src/entities/user.rs

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ use serde::{Deserialize, Serialize};
1212

1313
use crate::users::UserPreferences;
1414

15-
fn get_hasher() -> Argon2<'static> {
16-
Argon2::default()
17-
}
18-
1915
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)]
2016
#[graphql(name = "User")]
2117
#[sea_orm(table_name = "user")]
@@ -27,7 +23,6 @@ pub struct Model {
2723
pub password: Option<String>,
2824
pub oidc_issuer_id: Option<String>,
2925
pub created_on: DateTimeUtc,
30-
pub is_demo: Option<bool>,
3126
pub lot: UserLot,
3227
pub is_disabled: Option<bool>,
3328
#[graphql(skip)]
@@ -157,7 +152,7 @@ impl ActiveModelBehavior for ActiveModel {
157152
let cloned_password = self.password.clone().unwrap();
158153
if let Some(password) = cloned_password {
159154
let salt = SaltString::generate(&mut OsRng);
160-
let password_hash = get_hasher()
155+
let password_hash = Argon2::default()
161156
.hash_password(password.as_bytes(), &salt)
162157
.map_err(|_| DbErr::Custom("Unable to hash password".to_owned()))?
163158
.to_string();

apps/backend/src/entities/workout.rs

+17-11
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,33 @@ pub struct Model {
3434
pub summary: WorkoutSummary,
3535
pub information: WorkoutInformation,
3636
pub name: String,
37-
pub comment: Option<String>,
3837
}
3938

4039
#[async_trait]
4140
impl GraphqlRepresentation for Model {
42-
async fn graphql_representation(self, file_storage_service: &Arc<FileStorageService>) -> Result<Self> {
41+
async fn graphql_representation(
42+
self,
43+
file_storage_service: &Arc<FileStorageService>,
44+
) -> Result<Self> {
4345
let mut cnv_workout = self.clone();
44-
for image in cnv_workout.information.assets.images.iter_mut() {
45-
*image = file_storage_service.get_presigned_url(image.clone()).await;
46-
}
47-
for video in cnv_workout.information.assets.videos.iter_mut() {
48-
*video = file_storage_service.get_presigned_url(video.clone()).await;
49-
}
50-
for exercise in cnv_workout.information.exercises.iter_mut() {
51-
for image in exercise.assets.images.iter_mut() {
46+
if let Some(ref mut assets) = cnv_workout.information.assets {
47+
for image in assets.images.iter_mut() {
5248
*image = file_storage_service.get_presigned_url(image.clone()).await;
5349
}
54-
for video in exercise.assets.videos.iter_mut() {
50+
for video in assets.videos.iter_mut() {
5551
*video = file_storage_service.get_presigned_url(video.clone()).await;
5652
}
5753
}
54+
for exercise in cnv_workout.information.exercises.iter_mut() {
55+
if let Some(ref mut assets) = exercise.assets {
56+
for image in assets.images.iter_mut() {
57+
*image = file_storage_service.get_presigned_url(image.clone()).await;
58+
}
59+
for video in assets.videos.iter_mut() {
60+
*video = file_storage_service.get_presigned_url(video.clone()).await;
61+
}
62+
}
63+
}
5864
Ok(cnv_workout)
5965
}
6066
}

apps/backend/src/exporter.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use apalis::prelude::MessageQueue;
44
use async_graphql::{Context, Error, Object, Result, SimpleObject};
55
use chrono::{DateTime, Utc};
66
use nanoid::nanoid;
7-
use reqwest::{Body, Client};
7+
use reqwest::{
8+
header::{CONTENT_LENGTH, CONTENT_TYPE},
9+
Body, Client,
10+
};
811
use rs_utils::IsFeatureEnabled;
912
use sea_orm::prelude::DateTimeUtc;
1013
use serde::{Deserialize, Serialize};
@@ -146,7 +149,7 @@ impl ExporterService {
146149
writer.end_object().unwrap();
147150
writer.finish_document().unwrap();
148151
let ended_at = Utc::now();
149-
let (_, url) = self
152+
let (_key, url) = self
150153
.file_storage_service
151154
.get_presigned_put_url(
152155
export_path
@@ -155,7 +158,7 @@ impl ExporterService {
155158
.to_str()
156159
.unwrap()
157160
.to_string(),
158-
format!("exports/user__{}", user_id),
161+
format!("exports/{}", user_id),
159162
false,
160163
Some(HashMap::from([
161164
("started_at".to_string(), started_at.to_rfc2822()),
@@ -168,11 +171,15 @@ impl ExporterService {
168171
)
169172
.await;
170173
let file = File::open(&export_path).await.unwrap();
174+
let content_length = file.metadata().await.unwrap().len();
175+
let content_type = mime_guess::from_path(&export_path).first_or_octet_stream();
171176
let stream = FramedRead::new(file, BytesCodec::new());
172177
let body = Body::wrap_stream(stream);
173178
let client = Client::new();
174179
client
175180
.put(url)
181+
.header(CONTENT_TYPE, content_type.to_string())
182+
.header(CONTENT_LENGTH, content_length)
176183
.header("x-amz-meta-started_at", started_at.to_rfc2822())
177184
.header("x-amz-meta-ended_at", ended_at.to_rfc2822())
178185
.header(
@@ -193,7 +200,7 @@ impl ExporterService {
193200
let mut resp = vec![];
194201
let objects = self
195202
.file_storage_service
196-
.list_objects_at_prefix(format!("exports/user__{}", user_id))
203+
.list_objects_at_prefix(format!("exports/{}", user_id))
197204
.await;
198205
for object in objects {
199206
let url = self

apps/backend/src/fitness/logic.rs

+35-29
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,11 @@ impl UserWorkoutInput {
198198
totals.weight = Some(we * re);
199199
}
200200
let mut value = WorkoutSetRecord {
201-
totals,
202201
lot: set.lot,
203202
actual_rest_time,
203+
totals: Some(totals),
204204
note: set.note.clone(),
205-
personal_bests: vec![],
205+
personal_bests: Some(vec![]),
206206
confirmed_at: set.confirmed_at,
207207
statistic: set.statistic.clone(),
208208
};
@@ -244,32 +244,38 @@ impl UserWorkoutInput {
244244
let workout_set =
245245
workout.information.exercises[r.exercise_idx].sets[r.set_idx].clone();
246246
if set.get_personal_best(best_type) > workout_set.get_personal_best(best_type) {
247-
set.personal_bests.push(*best_type);
247+
if let Some(ref mut set_personal_bests) = set.personal_bests {
248+
set_personal_bests.push(*best_type);
249+
}
248250
total.personal_bests_achieved += 1;
249251
}
250252
} else {
251-
set.personal_bests.push(*best_type);
253+
if let Some(ref mut set_personal_bests) = set.personal_bests {
254+
set_personal_bests.push(*best_type);
255+
}
252256
total.personal_bests_achieved += 1;
253257
}
254258
}
255259
workout_totals.push(total.clone());
256260
for (set_idx, set) in sets.iter().enumerate() {
257-
for best in set.personal_bests.iter() {
258-
let to_insert_record = ExerciseBestSetRecord {
259-
workout_id: id.clone(),
260-
exercise_idx,
261-
set_idx,
262-
};
263-
if let Some(record) = personal_bests.iter_mut().find(|pb| pb.lot == *best) {
264-
let mut data =
265-
LengthVec::from_vec_and_length(record.sets.clone(), save_history);
266-
data.push_front(to_insert_record);
267-
record.sets = data.into_vec();
268-
} else {
269-
personal_bests.push(UserToExerciseBestSetExtraInformation {
270-
lot: *best,
271-
sets: vec![to_insert_record],
272-
});
261+
if let Some(set_personal_bests) = &set.personal_bests {
262+
for best in set_personal_bests.iter() {
263+
let to_insert_record = ExerciseBestSetRecord {
264+
workout_id: id.clone(),
265+
exercise_idx,
266+
set_idx,
267+
};
268+
if let Some(record) = personal_bests.iter_mut().find(|pb| pb.lot == *best) {
269+
let mut data =
270+
LengthVec::from_vec_and_length(record.sets.clone(), save_history);
271+
data.push_front(to_insert_record);
272+
record.sets = data.into_vec();
273+
} else {
274+
personal_bests.push(UserToExerciseBestSetExtraInformation {
275+
lot: *best,
276+
sets: vec![to_insert_record],
277+
});
278+
}
273279
}
274280
}
275281
}
@@ -286,40 +292,40 @@ impl UserWorkoutInput {
286292
exercises.push((
287293
db_ex.lot,
288294
ProcessedExercise {
289-
name: db_ex.id,
290-
lot: db_ex.lot,
291295
sets,
296+
lot: db_ex.lot,
297+
name: db_ex.id,
298+
total: Some(total),
292299
notes: ex.notes.clone(),
293300
rest_time: ex.rest_time,
294301
assets: ex.assets.clone(),
295302
superset_with: ex.superset_with.clone(),
296-
total,
297303
},
298304
));
299305
}
300-
let summary_total = workout_totals.into_iter().sum();
306+
let summary_total = Some(workout_totals.into_iter().sum());
301307
let model = workout::Model {
302308
id,
303309
end_time: input.end_time,
304310
start_time: input.start_time,
305311
repeated_from: input.repeated_from,
306312
user_id: user_id.clone(),
307313
name: input.name,
308-
comment: input.comment,
309314
summary: WorkoutSummary {
310315
total: summary_total,
311316
exercises: exercises
312317
.iter()
313318
.map(|(lot, e)| WorkoutSummaryExercise {
314-
num_sets: e.sets.len(),
319+
lot: Some(*lot),
315320
id: e.name.clone(),
316-
lot: *lot,
317-
best_set: e.sets[get_best_set_index(&e.sets).unwrap()].clone(),
321+
num_sets: e.sets.len(),
322+
best_set: Some(e.sets[get_best_set_index(&e.sets).unwrap()].clone()),
318323
})
319324
.collect(),
320325
},
321326
information: WorkoutInformation {
322-
assets: input.assets.clone(),
327+
comment: input.comment,
328+
assets: input.assets,
323329
exercises: exercises.into_iter().map(|(_, ex)| ex).collect(),
324330
},
325331
};

apps/backend/src/fitness/resolver.rs

+8-9
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@ impl ExerciseQuery {
141141
}
142142

143143
/// Get a paginated list of workouts done by the user.
144-
async fn user_workout_list(
144+
async fn user_workouts_list(
145145
&self,
146146
gql_ctx: &Context<'_>,
147147
input: SearchInput,
148148
) -> Result<SearchResults<WorkoutListItem>> {
149149
let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
150150
let user_id = self.user_id_from_ctx(gql_ctx).await?;
151-
service.user_workout_list(user_id, input).await
151+
service.user_workouts_list(user_id, input).await
152152
}
153153

154154
/// Get details about an exercise.
@@ -403,7 +403,7 @@ impl ExerciseService {
403403
Ok(resp)
404404
}
405405

406-
async fn user_workout_list(
406+
async fn user_workouts_list(
407407
&self,
408408
user_id: String,
409409
input: SearchInput,
@@ -793,9 +793,8 @@ impl ExerciseService {
793793
}
794794

795795
pub async fn delete_user_workout(&self, user_id: String, workout_id: String) -> Result<bool> {
796-
if let Some(wkt) = Workout::find()
796+
if let Some(wkt) = Workout::find_by_id(workout_id)
797797
.filter(workout::Column::UserId.eq(&user_id))
798-
.filter(workout::Column::Id.eq(workout_id))
799798
.one(&self.db)
800799
.await?
801800
{
@@ -829,12 +828,13 @@ impl ExerciseService {
829828

830829
pub fn db_workout_to_workout_input(&self, user_workout: workout::Model) -> UserWorkoutInput {
831830
UserWorkoutInput {
832-
id: Some(user_workout.id),
833831
name: user_workout.name,
834-
comment: user_workout.comment,
832+
id: Some(user_workout.id),
833+
end_time: user_workout.end_time,
835834
start_time: user_workout.start_time,
835+
assets: user_workout.information.assets,
836836
repeated_from: user_workout.repeated_from,
837-
end_time: user_workout.end_time,
837+
comment: user_workout.information.comment,
838838
exercises: user_workout
839839
.information
840840
.exercises
@@ -857,7 +857,6 @@ impl ExerciseService {
857857
superset_with: e.superset_with,
858858
})
859859
.collect(),
860-
assets: user_workout.information.assets,
861860
}
862861
}
863862

apps/backend/src/importer/strong_app.rs

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ use serde::{Deserialize, Serialize};
1212
use crate::{
1313
importer::{utils, DeployStrongAppImportInput, ImportResult},
1414
models::fitness::{
15-
EntityAssets, SetLot, UserExerciseInput, UserWorkoutInput, UserWorkoutSetRecord,
16-
WorkoutSetStatistic,
15+
SetLot, UserExerciseInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutSetStatistic,
1716
},
1817
};
1918

@@ -99,8 +98,8 @@ pub async fn import(
9998
exercise_id: target_exercise.target_name.clone(),
10099
sets,
101100
notes,
101+
assets: None,
102102
rest_time: None,
103-
assets: EntityAssets::default(),
104103
superset_with: vec![],
105104
});
106105
sets = vec![];
@@ -130,7 +129,7 @@ pub async fn import(
130129
start_time: ndt,
131130
end_time: ndt + workout_duration,
132131
exercises,
133-
assets: EntityAssets::default(),
132+
assets: None,
134133
});
135134
exercises = vec![];
136135
}

0 commit comments

Comments
 (0)