First commit
This commit is contained in:
31
db/createdb.sql
Normal file
31
db/createdb.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
create table exercise (
|
||||
id smallserial primary key,
|
||||
name varchar not null
|
||||
);
|
||||
create table shorthand (
|
||||
exercise smallint not null,
|
||||
name varchar unique not null,
|
||||
constraint fk_exercise foreign key (exercise) references exercise(id)
|
||||
);
|
||||
create table account (
|
||||
id serial primary key,
|
||||
name varchar not null,
|
||||
login varchar not null,
|
||||
birthdate date not null
|
||||
);
|
||||
create table training (
|
||||
id serial primary key,
|
||||
account integer not null,
|
||||
time timestamptz not null default timezone('utc', now()),
|
||||
exercise smallint not null,
|
||||
runs smallint not null,
|
||||
reps smallint not null,
|
||||
kilos numeric(4,1) not null,
|
||||
constraint fk_exercise foreign key (exercise) references exercise(id),
|
||||
constraint fk_account foreign key (account) references account(id)
|
||||
);
|
||||
create view dailylift as select
|
||||
date_trunc('day', time) as time, account, exercise, sum(runs * reps * kilos) as lift
|
||||
from training group by 1, 2, 3;
|
||||
|
||||
|
||||
5
db/drop.sql
Normal file
5
db/drop.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
drop view dailylift;
|
||||
drop table training;
|
||||
drop table shorthand;
|
||||
drop table exercise;
|
||||
drop table account;
|
||||
13
db/load.sql
Normal file
13
db/load.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
insert into exercise (name) values ('Bench Press');
|
||||
insert into shorthand values(currval('exercise_id_seq'), 'bp');
|
||||
insert into exercise (name) values ('Abdominals');
|
||||
insert into shorthand values(currval('exercise_id_seq'), 'abs');
|
||||
insert into exercise (name) values ('Squat');
|
||||
insert into shorthand values(currval('exercise_id_seq'), 'squat');
|
||||
insert into exercise (name) values ('Biceps');
|
||||
insert into shorthand values(currval('exercise_id_seq'), 'biceps');
|
||||
insert into exercise (name) values ('Triceps');
|
||||
insert into shorthand values(currval('exercise_id_seq'), 'triceps');
|
||||
insert into account (name, login, birthdate) values ('Jakob Dalsgaard', 'jakob', '1975-03-08');
|
||||
|
||||
|
||||
63
readme.md
Normal file
63
readme.md
Normal file
@@ -0,0 +1,63 @@
|
||||
Training Database
|
||||
=================
|
||||
|
||||
Toolset for registrering weight training with a focus on ease of registrering from
|
||||
a command line.
|
||||
|
||||
To install, start by creating the database:
|
||||
|
||||
```
|
||||
create database training;
|
||||
create user 'jakob';
|
||||
```
|
||||
|
||||
Edit `pg_hba.conf` to have 'jakob' access the training database directly:
|
||||
|
||||
```
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
local training jakob peer
|
||||
```
|
||||
|
||||
As postgres user, do a config reload:
|
||||
|
||||
```
|
||||
select pg_reload_conf();
|
||||
```
|
||||
|
||||
Then install rust toolchain by rustup.rs -- make and install the binary (I am assuming you have
|
||||
a `bin` directly in your homedir, that is in your path):
|
||||
|
||||
```
|
||||
cd train-cli
|
||||
cargo build --release
|
||||
cp target/release/train-cli $HOME/bin/train
|
||||
```
|
||||
|
||||
Right now the executable is hardcoded with database name `training` and the use
|
||||
of unix domain socket in `/var/run/postgresql`.
|
||||
|
||||
Now training can be invoked with:
|
||||
|
||||
```
|
||||
train squat 3 10 60
|
||||
```
|
||||
|
||||
Which would register squauts, 3 runs of 10 reps of 60kg -- at local time and date. Optionally a
|
||||
time, date time or rfc3339 timestamp can be specified:
|
||||
|
||||
```
|
||||
train squat 3 10 60 "12:05:00"
|
||||
train squat 3 10 60 "2025-12-24 18:00:00"
|
||||
train squat 3 10 60 "2025-12-24T18:00:00+0200"
|
||||
```
|
||||
|
||||
The two former will source missing date and timezone information from the user session, i.e.
|
||||
type `date` on your commandline to see what you have.
|
||||
|
||||
When travelling you might opt for specifying a specific location on the command line, like:
|
||||
|
||||
```
|
||||
TZ=Australia/Sydney train squat 3 10 60
|
||||
```
|
||||
|
||||
Data in the database can be visualized with, for example, Grafana, more info to follow.
|
||||
16
train-cli/Cargo.toml
Normal file
16
train-cli/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "train-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
postgres = { version = "0.19.10", features = ["with-chrono-0_4"] }
|
||||
sqlx = { version = "0.8.5", features = ["rust_decimal"] }
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
whoami = "1.6.0"
|
||||
rust_decimal = { version = "1.37.1", features = ["db-postgres"] }
|
||||
chrono = "0.4.41"
|
||||
|
||||
|
||||
82
train-cli/src/main.rs
Normal file
82
train-cli/src/main.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use clap::Parser;
|
||||
use postgres::{Client, NoTls};
|
||||
use whoami;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
use chrono::{DateTime, Local, FixedOffset, TimeZone, NaiveTime, LocalResult, NaiveDateTime};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct TrainingArgs {
|
||||
// name of exercise
|
||||
exercise: String,
|
||||
|
||||
// number of runs
|
||||
runs: i16,
|
||||
|
||||
// number of reps per run
|
||||
reps: i16,
|
||||
|
||||
// kilos
|
||||
kilos: f32,
|
||||
|
||||
// time
|
||||
time: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = TrainingArgs::parse();
|
||||
insert_training(args);
|
||||
}
|
||||
|
||||
fn parse_time (time: String) -> Option<DateTime<FixedOffset>> {
|
||||
// check if time is an rfc3339 formatted timestamp, i.e. "2025-05-20T14:30:10+0200" (can also
|
||||
// have milliseconds
|
||||
let datetimetz = DateTime::parse_from_rfc3339(&time);
|
||||
if let Ok(dt) = datetimetz {
|
||||
return Some(dt);
|
||||
}
|
||||
// check if time is a simple "date time" without timezone, then apply user session
|
||||
// time zone
|
||||
let naive_datetime = NaiveDateTime::parse_from_str(&time, "%Y-%m-%d %H:%M:%S");
|
||||
if let Ok(ndt) = naive_datetime {
|
||||
let dt: DateTime<Local> = Local.from_local_datetime(&ndt).unwrap();
|
||||
return Some(dt.into());
|
||||
}
|
||||
// check if time is merely a simple hour:minute:second -- then apply
|
||||
// current date from user session
|
||||
let naive_time = NaiveTime::parse_from_str(&time, "%H:%M:%S");
|
||||
if let Ok(nt) = naive_time {
|
||||
return match Local::now().with_time(nt) {
|
||||
LocalResult::Single(dt) => Some(dt.into()),
|
||||
LocalResult::Ambiguous(earliest, _latest) => Some(earliest.into()),
|
||||
LocalResult::None => None
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
fn insert_training (args: TrainingArgs) {
|
||||
let mut client = Client::connect("dbname=training host=/var/run/postgresql", NoTls).unwrap();
|
||||
let dec_kilos = Decimal::from_f32(args.kilos).unwrap();
|
||||
let dt = match args.time {
|
||||
Some(time) => parse_time(time),
|
||||
None => Some(Local::now().into()),
|
||||
};
|
||||
|
||||
match dt {
|
||||
None => println!("Invalid time/date specified"),
|
||||
Some(time) => {
|
||||
let res = client.execute("insert into training values
|
||||
(default, (select id from account where login=$1), $2,
|
||||
(select e.id from exercise e inner join shorthand s on s.exercise=e.id where s.name=$3), $4, $5, $6)",
|
||||
&[&whoami::username(), &time, &args.exercise, &args.runs, &args.reps, &dec_kilos]);
|
||||
match res {
|
||||
Ok(_inserted) => println!("Training inserted"),
|
||||
Err(e) => println!("Training not inserted, since: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user