2024-01-20 16:16:31 +00:00
|
|
|
/*
|
|
|
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
|
|
|
|
|
|
This file is part of FitnessTrax.
|
|
|
|
|
|
|
|
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
|
|
|
|
General Public License as published by the Free Software Foundation, either version 3 of the
|
|
|
|
License, or (at your option) any later version.
|
|
|
|
|
|
|
|
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
|
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2024-02-08 04:36:03 +00:00
|
|
|
#[allow(unused_imports)]
|
2024-02-03 20:28:33 +00:00
|
|
|
use crate::app::{ReadError, RecordProvider, WriteError};
|
2024-02-08 04:36:03 +00:00
|
|
|
#[allow(unused_imports)]
|
2024-02-01 15:12:35 +00:00
|
|
|
use chrono::NaiveDate;
|
2024-02-03 20:28:33 +00:00
|
|
|
use dimensioned::si;
|
2024-01-20 16:16:31 +00:00
|
|
|
use emseries::{Record, RecordId, Recordable};
|
2024-02-08 04:12:01 +00:00
|
|
|
use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord};
|
2024-01-20 16:16:31 +00:00
|
|
|
use std::{
|
|
|
|
collections::HashMap,
|
|
|
|
ops::Deref,
|
|
|
|
sync::{Arc, RwLock},
|
|
|
|
};
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
enum RecordState<T: Clone + Recordable> {
|
|
|
|
Original(Record<T>),
|
|
|
|
New(T),
|
|
|
|
Updated(Record<T>),
|
|
|
|
Deleted(Record<T>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T: Clone + emseries::Recordable> RecordState<T> {
|
2024-02-07 14:29:08 +00:00
|
|
|
fn exists(&self) -> bool {
|
|
|
|
match self {
|
2024-02-08 04:12:01 +00:00
|
|
|
RecordState::Original(_) => true,
|
|
|
|
RecordState::New(_) => true,
|
|
|
|
RecordState::Updated(_) => true,
|
|
|
|
RecordState::Deleted(_) => false,
|
2024-02-07 14:29:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-21 15:50:18 +00:00
|
|
|
#[allow(unused)]
|
|
|
|
fn data(&self) -> Option<&Record<T>> {
|
|
|
|
match self {
|
|
|
|
RecordState::Original(ref r) => Some(r),
|
|
|
|
RecordState::New(ref r) => None,
|
|
|
|
RecordState::Updated(ref r) => Some(r),
|
|
|
|
RecordState::Deleted(ref r) => Some(r),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-08 04:12:01 +00:00
|
|
|
fn set_value(&mut self, value: T) {
|
|
|
|
*self = match self {
|
|
|
|
RecordState::Original(r) => RecordState::Updated(Record {
|
|
|
|
id: r.id.clone(),
|
|
|
|
data: value,
|
|
|
|
}),
|
2024-01-20 16:16:31 +00:00
|
|
|
RecordState::New(_) => RecordState::New(value),
|
2024-02-08 04:12:01 +00:00
|
|
|
RecordState::Updated(r) => RecordState::Updated(Record {
|
|
|
|
id: r.id.clone(),
|
|
|
|
data: value,
|
|
|
|
}),
|
|
|
|
RecordState::Deleted(r) => RecordState::Updated(Record {
|
|
|
|
id: r.id.clone(),
|
|
|
|
data: value,
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn with_value(mut self, value: T) -> RecordState<T> {
|
|
|
|
self.set_value(value);
|
|
|
|
self
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
|
|
|
|
2024-01-20 19:35:10 +00:00
|
|
|
#[allow(unused)]
|
2024-01-20 16:16:31 +00:00
|
|
|
fn with_delete(self) -> Option<RecordState<T>> {
|
|
|
|
match self {
|
|
|
|
RecordState::Original(r) => Some(RecordState::Deleted(r)),
|
|
|
|
RecordState::New(r) => None,
|
|
|
|
RecordState::Updated(r) => Some(RecordState::Deleted(r)),
|
|
|
|
RecordState::Deleted(r) => Some(RecordState::Deleted(r)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
|
|
|
|
type Target = T;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
match self {
|
|
|
|
RecordState::Original(ref r) => &r.data,
|
2024-01-20 19:35:10 +00:00
|
|
|
RecordState::New(ref r) => r,
|
2024-01-20 16:16:31 +00:00
|
|
|
RecordState::Updated(ref r) => &r.data,
|
|
|
|
RecordState::Deleted(ref r) => &r.data,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
impl<T: Clone + emseries::Recordable> std::ops::DerefMut for RecordState<T> {
|
|
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
|
|
match self {
|
|
|
|
RecordState::Original(ref mut r) => &mut r.data,
|
|
|
|
RecordState::New(ref mut r) => r,
|
|
|
|
RecordState::Updated(ref mut r) => &mut r.data,
|
|
|
|
RecordState::Deleted(ref mut r) => &mut r.data,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-29 14:18:36 +00:00
|
|
|
#[derive(Clone)]
|
2024-01-20 16:16:31 +00:00
|
|
|
pub struct DayDetailViewModel {
|
2024-02-01 15:12:35 +00:00
|
|
|
provider: Arc<dyn RecordProvider>,
|
2024-01-20 16:16:31 +00:00
|
|
|
pub date: chrono::NaiveDate,
|
|
|
|
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
|
|
|
|
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
|
|
|
|
records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DayDetailViewModel {
|
2024-02-01 15:12:35 +00:00
|
|
|
pub async fn new(
|
|
|
|
date: chrono::NaiveDate,
|
|
|
|
provider: impl RecordProvider + 'static,
|
|
|
|
) -> Result<Self, ReadError> {
|
2024-01-20 16:16:31 +00:00
|
|
|
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
2024-02-01 15:12:35 +00:00
|
|
|
provider
|
|
|
|
.records(date, date)
|
|
|
|
.await?
|
|
|
|
.into_iter()
|
|
|
|
.partition(|r| r.data.is_weight());
|
2024-01-20 16:16:31 +00:00
|
|
|
let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
|
|
|
records.into_iter().partition(|r| r.data.is_steps());
|
2024-02-07 13:28:38 +00:00
|
|
|
|
|
|
|
if weight_records.len() > 1 {
|
|
|
|
eprintln!("warning: multiple weight records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d"));
|
|
|
|
}
|
|
|
|
if step_records.len() > 1 {
|
|
|
|
eprintln!("warning: multiple step records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d"));
|
|
|
|
}
|
|
|
|
|
2024-02-01 15:12:35 +00:00
|
|
|
Ok(Self {
|
|
|
|
provider: Arc::new(provider),
|
2024-01-20 16:16:31 +00:00
|
|
|
date,
|
|
|
|
weight: Arc::new(RwLock::new(
|
|
|
|
weight_records
|
|
|
|
.first()
|
|
|
|
.and_then(|r| match r.data {
|
|
|
|
TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map(|(id, w)| RecordState::Original(Record { id, data: w })),
|
|
|
|
)),
|
|
|
|
steps: Arc::new(RwLock::new(
|
|
|
|
step_records
|
|
|
|
.first()
|
|
|
|
.and_then(|r| match r.data {
|
|
|
|
TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map(|(id, w)| RecordState::Original(Record { id, data: w })),
|
|
|
|
)),
|
|
|
|
|
|
|
|
records: Arc::new(RwLock::new(
|
|
|
|
records
|
|
|
|
.into_iter()
|
|
|
|
.map(|r| (r.id.clone(), RecordState::Original(r)))
|
|
|
|
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>(),
|
|
|
|
)),
|
2024-02-01 15:12:35 +00:00
|
|
|
})
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
pub fn weight(&self) -> Option<si::Kilogram<f64>> {
|
2024-02-08 04:36:03 +00:00
|
|
|
(*self.weight.read().unwrap()).as_ref().map(|w| w.weight)
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
pub fn set_weight(&self, new_weight: si::Kilogram<f64>) {
|
2024-01-20 16:16:31 +00:00
|
|
|
let mut record = self.weight.write().unwrap();
|
|
|
|
let new_record = match *record {
|
|
|
|
Some(ref rstate) => rstate.clone().with_value(ft_core::Weight {
|
2024-01-20 19:35:10 +00:00
|
|
|
date: self.date,
|
2024-02-03 20:28:33 +00:00
|
|
|
weight: new_weight,
|
2024-01-20 16:16:31 +00:00
|
|
|
}),
|
|
|
|
None => RecordState::New(ft_core::Weight {
|
2024-01-20 19:35:10 +00:00
|
|
|
date: self.date,
|
2024-02-03 20:28:33 +00:00
|
|
|
weight: new_weight,
|
2024-01-20 16:16:31 +00:00
|
|
|
}),
|
|
|
|
};
|
|
|
|
*record = Some(new_record);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn steps(&self) -> Option<u32> {
|
2024-01-20 19:35:10 +00:00
|
|
|
(*self.steps.read().unwrap()).as_ref().map(|w| w.count)
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_steps(&self, new_count: u32) {
|
|
|
|
let mut record = self.steps.write().unwrap();
|
|
|
|
let new_record = match *record {
|
|
|
|
Some(ref rstate) => rstate.clone().with_value(ft_core::Steps {
|
2024-01-20 19:35:10 +00:00
|
|
|
date: self.date,
|
2024-01-20 16:16:31 +00:00
|
|
|
count: new_count,
|
|
|
|
}),
|
|
|
|
None => RecordState::New(ft_core::Steps {
|
2024-01-20 19:35:10 +00:00
|
|
|
date: self.date,
|
2024-01-20 16:16:31 +00:00
|
|
|
count: new_count,
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
*record = Some(new_record);
|
|
|
|
}
|
|
|
|
|
2024-02-08 04:12:01 +00:00
|
|
|
pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record<TimeDistance> {
|
2024-02-03 20:28:33 +00:00
|
|
|
let id = RecordId::default();
|
|
|
|
let workout = TimeDistance {
|
|
|
|
datetime: chrono::Local::now().into(),
|
2024-02-08 04:12:01 +00:00
|
|
|
activity,
|
2024-02-03 20:28:33 +00:00
|
|
|
distance: None,
|
|
|
|
duration: None,
|
|
|
|
comments: None,
|
|
|
|
};
|
2024-02-08 04:12:01 +00:00
|
|
|
let tr = TraxRecord::from(workout.clone());
|
2024-02-03 20:28:33 +00:00
|
|
|
self.records
|
|
|
|
.write()
|
|
|
|
.unwrap()
|
|
|
|
.insert(id.clone(), RecordState::New(tr));
|
|
|
|
println!(
|
|
|
|
"records after new_time_distance: {:?}",
|
|
|
|
self.records.read().unwrap()
|
|
|
|
);
|
|
|
|
Record { id, data: workout }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_time_distance(&self, workout: Record<TimeDistance>) {
|
|
|
|
let id = workout.id.clone();
|
|
|
|
let data = workout.data.clone();
|
|
|
|
|
2024-02-07 14:29:08 +00:00
|
|
|
let mut record_set = self.records.write().unwrap();
|
2024-02-08 04:12:01 +00:00
|
|
|
record_set.entry(id).and_modify(|record_state| {
|
|
|
|
record_state.set_value(TraxRecord::TimeDistance(data));
|
|
|
|
});
|
2024-02-03 20:28:33 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 04:12:01 +00:00
|
|
|
pub fn time_distance_records(&self) -> Vec<Record<TimeDistance>> {
|
2024-02-03 20:28:33 +00:00
|
|
|
self.records
|
|
|
|
.read()
|
|
|
|
.unwrap()
|
|
|
|
.iter()
|
2024-02-07 14:29:08 +00:00
|
|
|
.filter(|(_, record)| record.exists())
|
2024-02-03 20:28:33 +00:00
|
|
|
.filter_map(|(id, record_state)| match **record_state {
|
2024-02-08 04:12:01 +00:00
|
|
|
TraxRecord::TimeDistance(ref workout) => Some(Record {
|
2024-02-07 14:29:08 +00:00
|
|
|
id: id.clone(),
|
|
|
|
data: workout.clone(),
|
|
|
|
}),
|
2024-02-03 20:28:33 +00:00
|
|
|
_ => None,
|
|
|
|
})
|
2024-02-07 14:29:08 +00:00
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn time_distance_summary(
|
|
|
|
&self,
|
2024-02-08 04:12:01 +00:00
|
|
|
activity: TimeDistanceActivity,
|
2024-02-07 14:29:08 +00:00
|
|
|
) -> (si::Meter<f64>, si::Second<f64>) {
|
2024-02-08 04:12:01 +00:00
|
|
|
self.time_distance_records()
|
|
|
|
.into_iter()
|
|
|
|
.filter(|rec| rec.data.activity == activity)
|
|
|
|
.fold(
|
|
|
|
(0. * si::M, 0. * si::S),
|
|
|
|
|(distance, duration), workout| match (workout.data.distance, workout.data.duration)
|
|
|
|
{
|
|
|
|
(Some(distance_), Some(duration_)) => {
|
|
|
|
(distance + distance_, duration + duration_)
|
|
|
|
}
|
|
|
|
(Some(distance_), None) => (distance + distance_, duration),
|
|
|
|
(None, Some(duration_)) => (distance, duration + duration_),
|
|
|
|
(None, None) => (distance, duration),
|
|
|
|
},
|
|
|
|
)
|
2024-02-03 20:28:33 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 04:36:03 +00:00
|
|
|
#[allow(unused)]
|
2024-02-03 20:28:33 +00:00
|
|
|
fn get_record(&self, id: &RecordId) -> Option<Record<TraxRecord>> {
|
|
|
|
let record_set = self.records.read().unwrap();
|
2024-02-08 04:36:03 +00:00
|
|
|
record_set.get(id).map(|record| Record {
|
|
|
|
id: id.clone(),
|
|
|
|
data: (**record).clone(),
|
|
|
|
})
|
2024-02-03 20:28:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn remove_record(&self, id: RecordId) {
|
2024-02-07 14:29:08 +00:00
|
|
|
let mut record_set = self.records.write().unwrap();
|
|
|
|
let updated_record = match record_set.remove(&id) {
|
|
|
|
Some(RecordState::Original(r)) => Some(RecordState::Deleted(r)),
|
|
|
|
Some(RecordState::New(_)) => None,
|
|
|
|
Some(RecordState::Updated(r)) => Some(RecordState::Deleted(r)),
|
|
|
|
Some(RecordState::Deleted(r)) => Some(RecordState::Deleted(r)),
|
|
|
|
None => None,
|
|
|
|
};
|
|
|
|
if let Some(updated_record) = updated_record {
|
|
|
|
record_set.insert(id, updated_record);
|
|
|
|
}
|
2024-02-03 20:28:33 +00:00
|
|
|
}
|
|
|
|
|
2024-01-20 16:16:31 +00:00
|
|
|
pub fn save(&self) {
|
|
|
|
glib::spawn_future({
|
|
|
|
let s = self.clone();
|
2024-02-03 20:28:33 +00:00
|
|
|
async move { s.async_save().await }
|
|
|
|
});
|
|
|
|
}
|
2024-01-20 22:04:20 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
pub async fn async_save(&self) {
|
|
|
|
println!("async_save");
|
|
|
|
let weight_record = self.weight.read().unwrap().clone();
|
|
|
|
match weight_record {
|
|
|
|
Some(RecordState::New(data)) => {
|
|
|
|
let _ = self.provider.put_record(TraxRecord::Weight(data)).await;
|
|
|
|
}
|
|
|
|
Some(RecordState::Original(_)) => {}
|
|
|
|
Some(RecordState::Updated(weight)) => {
|
|
|
|
let _ = self
|
|
|
|
.provider
|
|
|
|
.update_record(Record {
|
|
|
|
id: weight.id,
|
|
|
|
data: TraxRecord::Weight(weight.data),
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
Some(RecordState::Deleted(_)) => {}
|
|
|
|
None => {}
|
|
|
|
}
|
2024-01-20 16:16:31 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
let steps_record = self.steps.read().unwrap().clone();
|
|
|
|
match steps_record {
|
|
|
|
Some(RecordState::New(data)) => {
|
|
|
|
let _ = self.provider.put_record(TraxRecord::Steps(data)).await;
|
|
|
|
}
|
|
|
|
Some(RecordState::Original(_)) => {}
|
|
|
|
Some(RecordState::Updated(steps)) => {
|
|
|
|
let _ = self
|
|
|
|
.provider
|
|
|
|
.update_record(Record {
|
|
|
|
id: steps.id,
|
|
|
|
data: TraxRecord::Steps(steps.data),
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
Some(RecordState::Deleted(_)) => {}
|
|
|
|
None => {}
|
|
|
|
}
|
|
|
|
|
|
|
|
let records = self
|
|
|
|
.records
|
|
|
|
.write()
|
|
|
|
.unwrap()
|
|
|
|
.drain()
|
|
|
|
.map(|(_, record)| record)
|
|
|
|
.collect::<Vec<RecordState<TraxRecord>>>();
|
|
|
|
|
|
|
|
for record in records {
|
|
|
|
println!("saving record: {:?}", record);
|
|
|
|
match record {
|
|
|
|
RecordState::New(data) => {
|
|
|
|
let _ = self.provider.put_record(data).await;
|
|
|
|
}
|
|
|
|
RecordState::Original(_) => {}
|
|
|
|
RecordState::Updated(r) => {
|
|
|
|
let _ = self.provider.update_record(r.clone()).await;
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
2024-02-07 14:29:08 +00:00
|
|
|
RecordState::Deleted(r) => {
|
|
|
|
let _ = self.provider.delete_record(r.id).await;
|
|
|
|
}
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
2024-02-03 20:28:33 +00:00
|
|
|
}
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
|
|
|
|
2024-01-23 13:46:35 +00:00
|
|
|
pub async fn revert(&self) {
|
|
|
|
self.populate_records().await;
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn populate_records(&self) {
|
|
|
|
let records = self.provider.records(self.date, self.date).await.unwrap();
|
|
|
|
|
|
|
|
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
|
|
|
records.into_iter().partition(|r| r.data.is_weight());
|
|
|
|
let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
|
|
|
records.into_iter().partition(|r| r.data.is_steps());
|
|
|
|
|
|
|
|
*self.weight.write().unwrap() = weight_records
|
|
|
|
.first()
|
|
|
|
.and_then(|r| match r.data {
|
|
|
|
TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map(|(id, w)| RecordState::Original(Record { id, data: w }));
|
|
|
|
|
|
|
|
*self.steps.write().unwrap() = step_records
|
|
|
|
.first()
|
|
|
|
.and_then(|r| match r.data {
|
|
|
|
TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map(|(id, w)| RecordState::Original(Record { id, data: w }));
|
|
|
|
|
|
|
|
*self.records.write().unwrap() = records
|
|
|
|
.into_iter()
|
|
|
|
.map(|r| (r.id.clone(), RecordState::Original(r)))
|
|
|
|
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>();
|
2024-01-20 16:16:31 +00:00
|
|
|
}
|
|
|
|
}
|
2024-02-01 15:12:35 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
use async_trait::async_trait;
|
2024-01-23 13:46:35 +00:00
|
|
|
use chrono::{DateTime, FixedOffset};
|
2024-02-01 15:12:35 +00:00
|
|
|
use dimensioned::si;
|
|
|
|
use emseries::Record;
|
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2024-02-01 15:12:35 +00:00
|
|
|
struct MockProvider {
|
2024-02-03 20:28:33 +00:00
|
|
|
records: Arc<RwLock<HashMap<RecordId, Record<TraxRecord>>>>,
|
|
|
|
|
|
|
|
put_records: Arc<RwLock<Vec<Record<TraxRecord>>>>,
|
|
|
|
updated_records: Arc<RwLock<Vec<Record<TraxRecord>>>>,
|
|
|
|
deleted_records: Arc<RwLock<Vec<RecordId>>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MockProvider {
|
|
|
|
fn new(records: Vec<Record<TraxRecord>>) -> Self {
|
|
|
|
let record_map = records
|
|
|
|
.into_iter()
|
|
|
|
.map(|r| (r.id.clone(), r))
|
|
|
|
.collect::<HashMap<RecordId, Record<TraxRecord>>>();
|
|
|
|
Self {
|
|
|
|
records: Arc::new(RwLock::new(record_map)),
|
|
|
|
put_records: Arc::new(RwLock::new(vec![])),
|
|
|
|
updated_records: Arc::new(RwLock::new(vec![])),
|
|
|
|
deleted_records: Arc::new(RwLock::new(vec![])),
|
|
|
|
}
|
|
|
|
}
|
2024-02-01 15:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
impl RecordProvider for MockProvider {
|
|
|
|
async fn records(
|
|
|
|
&self,
|
|
|
|
start: NaiveDate,
|
|
|
|
end: NaiveDate,
|
|
|
|
) -> Result<Vec<Record<TraxRecord>>, ReadError> {
|
|
|
|
let start = emseries::Timestamp::Date(start);
|
|
|
|
let end = emseries::Timestamp::Date(end);
|
|
|
|
Ok(self
|
|
|
|
.records
|
2024-02-03 20:28:33 +00:00
|
|
|
.read()
|
|
|
|
.unwrap()
|
2024-02-01 15:12:35 +00:00
|
|
|
.iter()
|
2024-02-03 20:28:33 +00:00
|
|
|
.map(|(_, r)| r)
|
2024-02-01 15:12:35 +00:00
|
|
|
.filter(|r| r.timestamp() >= start && r.timestamp() <= end)
|
|
|
|
.cloned()
|
|
|
|
.collect::<Vec<Record<TraxRecord>>>())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
|
2024-02-03 20:28:33 +00:00
|
|
|
let id = RecordId::default();
|
|
|
|
let record = Record {
|
|
|
|
id: id.clone(),
|
|
|
|
data: record,
|
|
|
|
};
|
|
|
|
self.put_records.write().unwrap().push(record.clone());
|
|
|
|
self.records.write().unwrap().insert(id.clone(), record);
|
|
|
|
Ok(id)
|
2024-02-01 15:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
|
2024-02-07 14:29:08 +00:00
|
|
|
println!("updated record: {:?}", record);
|
|
|
|
self.updated_records.write().unwrap().push(record.clone());
|
|
|
|
self.records
|
|
|
|
.write()
|
|
|
|
.unwrap()
|
|
|
|
.insert(record.id.clone(), record);
|
|
|
|
Ok(())
|
2024-02-01 15:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> {
|
2024-02-07 14:29:08 +00:00
|
|
|
self.deleted_records.write().unwrap().push(id.clone());
|
|
|
|
let _ = self.records.write().unwrap().remove(&id);
|
|
|
|
Ok(())
|
2024-02-01 15:12:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
async fn create_empty_view_model() -> (DayDetailViewModel, MockProvider) {
|
|
|
|
let provider = MockProvider::new(vec![]);
|
|
|
|
|
|
|
|
let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
|
|
|
|
let model = DayDetailViewModel::new(oct_13, provider.clone())
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
(model, provider)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn create_view_model() -> (DayDetailViewModel, MockProvider) {
|
2024-02-01 15:12:35 +00:00
|
|
|
let oct_12 = chrono::NaiveDate::from_ymd_opt(2023, 10, 12).unwrap();
|
|
|
|
let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
|
2024-02-03 20:28:33 +00:00
|
|
|
let oct_13_am: DateTime<FixedOffset> = oct_13
|
|
|
|
.clone()
|
|
|
|
.and_hms_opt(3, 28, 0)
|
|
|
|
.unwrap()
|
|
|
|
.and_utc()
|
|
|
|
.with_timezone(&FixedOffset::east_opt(5 * 3600).unwrap());
|
|
|
|
let provider = MockProvider::new(vec![
|
|
|
|
Record {
|
|
|
|
id: RecordId::default(),
|
|
|
|
data: TraxRecord::Weight(ft_core::Weight {
|
2024-02-08 04:36:03 +00:00
|
|
|
date: oct_12,
|
2024-02-03 20:28:33 +00:00
|
|
|
weight: 93. * si::KG,
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
Record {
|
|
|
|
id: RecordId::default(),
|
|
|
|
data: TraxRecord::Weight(ft_core::Weight {
|
2024-02-08 04:36:03 +00:00
|
|
|
date: oct_13,
|
2024-02-03 20:28:33 +00:00
|
|
|
weight: 95. * si::KG,
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
Record {
|
|
|
|
id: RecordId::default(),
|
|
|
|
data: TraxRecord::Steps(ft_core::Steps {
|
2024-02-08 04:36:03 +00:00
|
|
|
date: oct_13,
|
2024-02-03 20:28:33 +00:00
|
|
|
count: 2500,
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
Record {
|
|
|
|
id: RecordId::default(),
|
2024-02-08 04:12:01 +00:00
|
|
|
data: TraxRecord::TimeDistance(ft_core::TimeDistance {
|
|
|
|
datetime: oct_13_am.clone(),
|
|
|
|
activity: TimeDistanceActivity::BikeRide,
|
2024-02-03 20:28:33 +00:00
|
|
|
distance: Some(15000. * si::M),
|
|
|
|
duration: Some(3600. * si::S),
|
|
|
|
comments: Some("somecomments present".to_owned()),
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
let model = DayDetailViewModel::new(oct_13, provider.clone())
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
(model, provider)
|
2024-02-01 15:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn it_honors_only_the_first_weight_and_step_record() {
|
2024-02-08 04:36:03 +00:00
|
|
|
let (view_model, _provider) = create_view_model().await;
|
2024-02-03 20:28:33 +00:00
|
|
|
assert_eq!(view_model.weight(), Some(95. * si::KG));
|
|
|
|
assert_eq!(view_model.steps(), Some(2500));
|
2024-02-01 15:12:35 +00:00
|
|
|
}
|
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn it_can_create_a_weight_and_stepcount() {
|
|
|
|
let (view_model, provider) = create_empty_view_model().await;
|
|
|
|
assert_eq!(view_model.weight(), None);
|
|
|
|
assert_eq!(view_model.steps(), None);
|
2024-02-01 15:12:35 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
view_model.set_weight(95. * si::KG);
|
|
|
|
view_model.set_steps(250);
|
2024-02-01 15:12:35 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
assert_eq!(view_model.weight(), Some(95. * si::KG));
|
|
|
|
assert_eq!(view_model.steps(), Some(250));
|
2024-02-01 15:12:35 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
view_model.set_weight(93. * si::KG);
|
|
|
|
view_model.set_steps(255);
|
|
|
|
|
|
|
|
assert_eq!(view_model.weight(), Some(93. * si::KG));
|
|
|
|
assert_eq!(view_model.steps(), Some(255));
|
|
|
|
|
|
|
|
view_model.async_save().await;
|
|
|
|
|
|
|
|
println!("provider: {:?}", provider);
|
|
|
|
assert_eq!(provider.put_records.read().unwrap().len(), 2);
|
|
|
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
|
|
|
}
|
2024-02-01 15:12:35 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn it_can_construct_new_records() {
|
|
|
|
let (view_model, provider) = create_empty_view_model().await;
|
|
|
|
assert_eq!(
|
2024-02-08 04:12:01 +00:00
|
|
|
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
2024-02-03 20:28:33 +00:00
|
|
|
(0. * si::M, 0. * si::S)
|
|
|
|
);
|
|
|
|
|
2024-02-08 04:12:01 +00:00
|
|
|
let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
2024-02-03 20:28:33 +00:00
|
|
|
record.data.duration = Some(60. * si::S);
|
|
|
|
view_model.async_save().await;
|
|
|
|
|
|
|
|
assert_eq!(provider.put_records.read().unwrap().len(), 1);
|
|
|
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn it_can_update_a_new_record_before_saving() {
|
|
|
|
let (view_model, provider) = create_empty_view_model().await;
|
|
|
|
assert_eq!(
|
2024-02-08 04:12:01 +00:00
|
|
|
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
2024-02-03 20:28:33 +00:00
|
|
|
(0. * si::M, 0. * si::S)
|
|
|
|
);
|
|
|
|
|
2024-02-08 04:12:01 +00:00
|
|
|
let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
2024-02-03 20:28:33 +00:00
|
|
|
record.data.duration = Some(60. * si::S);
|
|
|
|
view_model.update_time_distance(record.clone());
|
|
|
|
let record = Record {
|
|
|
|
id: record.id,
|
2024-02-08 04:12:01 +00:00
|
|
|
data: TraxRecord::TimeDistance(record.data),
|
2024-02-03 20:28:33 +00:00
|
|
|
};
|
|
|
|
assert_eq!(view_model.get_record(&record.id), Some(record));
|
|
|
|
assert_eq!(
|
2024-02-08 04:12:01 +00:00
|
|
|
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
2024-02-03 20:28:33 +00:00
|
|
|
(0. * si::M, 60. * si::S)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2024-02-08 04:12:01 +00:00
|
|
|
view_model.time_distance_summary(TimeDistanceActivity::Running),
|
2024-02-03 20:28:33 +00:00
|
|
|
(0. * si::M, 0. * si::S)
|
|
|
|
);
|
|
|
|
view_model.async_save().await;
|
|
|
|
|
|
|
|
assert_eq!(provider.put_records.read().unwrap().len(), 1);
|
|
|
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn it_can_update_an_existing_record() {
|
|
|
|
let (view_model, provider) = create_view_model().await;
|
2024-02-08 04:12:01 +00:00
|
|
|
let mut workout = view_model.time_distance_records().first().cloned().unwrap();
|
2024-02-03 20:28:33 +00:00
|
|
|
|
2024-02-07 14:29:08 +00:00
|
|
|
println!("found record: {:?}", workout);
|
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
workout.data.duration = Some(1800. * si::S);
|
|
|
|
view_model.update_time_distance(workout.clone());
|
|
|
|
|
|
|
|
assert_eq!(
|
2024-02-08 04:12:01 +00:00
|
|
|
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
2024-02-03 20:28:33 +00:00
|
|
|
(15000. * si::M, 1800. * si::S)
|
|
|
|
);
|
|
|
|
|
2024-02-07 14:29:08 +00:00
|
|
|
view_model.async_save().await;
|
2024-02-03 20:28:33 +00:00
|
|
|
|
|
|
|
assert_eq!(provider.put_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.updated_records.read().unwrap().len(), 1);
|
|
|
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
|
|
|
}
|
2024-02-01 15:12:35 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn it_can_remove_a_new_record() {
|
|
|
|
let (view_model, provider) = create_empty_view_model().await;
|
|
|
|
assert_eq!(
|
2024-02-08 04:12:01 +00:00
|
|
|
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
2024-02-03 20:28:33 +00:00
|
|
|
(0. * si::M, 0. * si::S)
|
|
|
|
);
|
|
|
|
|
2024-02-08 04:12:01 +00:00
|
|
|
let record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
2024-02-03 20:28:33 +00:00
|
|
|
view_model.remove_record(record.id);
|
|
|
|
view_model.save();
|
|
|
|
|
|
|
|
assert_eq!(provider.put_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
|
|
|
}
|
2024-02-01 15:12:35 +00:00
|
|
|
|
2024-02-03 20:28:33 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn it_can_delete_an_existing_record() {
|
|
|
|
let (view_model, provider) = create_view_model().await;
|
2024-01-23 13:46:35 +00:00
|
|
|
let workout = view_model.time_distance_records().first().cloned().unwrap();
|
2024-02-03 20:28:33 +00:00
|
|
|
|
|
|
|
view_model.remove_record(workout.id);
|
|
|
|
assert_eq!(
|
2024-02-08 04:12:01 +00:00
|
|
|
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
2024-02-03 20:28:33 +00:00
|
|
|
(0. * si::M, 0. * si::S)
|
|
|
|
);
|
2024-02-07 14:29:08 +00:00
|
|
|
view_model.async_save().await;
|
2024-02-03 20:28:33 +00:00
|
|
|
|
|
|
|
assert_eq!(provider.put_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
|
|
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 1);
|
|
|
|
}
|
2024-02-01 15:12:35 +00:00
|
|
|
}
|