feat: shifts

Oh boy what a mess this was lmao, I definitely should have done this is more atomic commits

But it could also be argued that very little of this is standalone
main
Zynh0722 2023-09-26 02:18:12 -07:00
parent 1515294500
commit 96dd76e463
18 changed files with 1860 additions and 13 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
.env

1606
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,10 @@ version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "cm_lib"
path = "src/lib/mod.rs"
[dependencies] [dependencies]
axum = "0.6.20" axum = "0.6.20"
@ -14,6 +18,12 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower-http = { version = "0.4.4", features = ["full"] } tower-http = { version = "0.4.4", features = ["full"] }
ructe.workspace = true ructe.workspace = true
tokio-stream = "0.1.14"
futures-util = "0.3.28"
diesel = { version = "2.1.0", features = ["chrono"] }
diesel-async = { version = "0.3.1", features = ["mysql", "deadpool"] }
dotenvy = "0.15"
chrono = "0.4.31"
[build-dependencies] [build-dependencies]
ructe.workspace = true ructe.workspace = true

9
diesel.toml Normal file
View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/lib/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

24
docker-compose.yml Normal file
View File

@ -0,0 +1,24 @@
# This DC is just to get a dev mysql db, nothing else.
# Use root/example as user/password credentials
version: '3.1'
services:
db:
image: mysql:8.0.34
restart: always
ports:
- 3306:3306
environment:
MYSQL_HOST: "%"
MYSQL_USER: clubmanager
MYSQL_PASSWORD: clubmanager
MYSQL_DATABASE: clubmanager
MYSQL_ROOT_PASSWORD: admin
TZ: America/Los_Angeles
adminer:
image: adminer
restart: always
ports:
- 8080:8080

4
example.env Normal file
View File

@ -0,0 +1,4 @@
# This defines the mysql db url that clubmanager will use
# The default shown here is what the docker-compose.yml will generate
# Please don't use it for prod
DATABASE_URL=mysql://clubmanager:clubmanager@127.0.0.1/clubmanager

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE shifts

View File

@ -0,0 +1,6 @@
CREATE TABLE shifts
(
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
start DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
end DATETIME DEFAULT NULL
)

5
mprocs.yaml Normal file
View File

@ -0,0 +1,5 @@
procs:
server:
shell: "cargo watch -x run"
docker:
shell: "docker compose up"

68
src/api.rs Normal file
View File

@ -0,0 +1,68 @@
use axum::{extract::State, response::IntoResponse, routing::post};
use cm_lib::models::Shift;
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
use diesel_async::{scoped_futures::ScopedFutureExt, AsyncConnection, RunQueryDsl};
use crate::axum_ructe::render;
use crate::AppState;
pub(crate) fn router() -> axum::Router<AppState> {
axum::Router::new()
.route("/shifts/open", post(open_shift))
.route("/shifts/close", post(close_shift))
}
async fn open_shift(State(state): State<AppState>) -> impl IntoResponse {
let shift = {
let mut conn = state.connection.get().await.unwrap();
conn.transaction(|conn| {
use cm_lib::schema::shifts::dsl::*;
async move {
diesel::insert_into(shifts)
.default_values()
.execute(conn)
.await?;
shifts
.order(id.desc())
.select(Shift::as_select())
.first(conn)
.await
}
.scope_boxed()
})
.await
.optional()
.unwrap()
};
render!(crate::templates::home_html, shift)
}
async fn close_shift(State(state): State<AppState>) -> impl IntoResponse {
{
let mut conn = state.connection.get().await.unwrap();
conn.transaction(|conn| {
use cm_lib::schema::shifts::dsl::*;
async move {
let open_shift = shifts
.filter(end.is_null())
.select(Shift::as_select())
.first(conn)
.await?;
diesel::update(shifts.filter(id.eq(open_shift.id)))
.set(end.eq(Some(chrono::Utc::now().naive_local())))
.execute(conn)
.await
}
.scope_boxed()
})
.await
.unwrap();
}
render!(crate::templates::home_html, None)
}

2
src/lib/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod models;
pub mod schema;

10
src/lib/models.rs Normal file
View File

@ -0,0 +1,10 @@
use diesel::prelude::*;
#[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = crate::schema::shifts)]
#[diesel(check_for_backend(diesel::mysql::Mysql))]
pub struct Shift {
pub id: u32,
pub start: chrono::NaiveDateTime,
pub end: Option<chrono::NaiveDateTime>,
}

9
src/lib/schema.rs Normal file
View File

@ -0,0 +1,9 @@
// @generated automatically by Diesel CLI.
diesel::table! {
shifts (id) {
id -> Unsigned<Integer>,
start -> Datetime,
end -> Nullable<Datetime>,
}
}

View File

@ -1,19 +1,54 @@
mod api;
mod axum_ructe; mod axum_ructe;
use axum_ructe::render; use axum_ructe::render;
use axum::{ use axum::{
extract::State,
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
routing::{get, post}, routing::{get, post},
Json, Router, Json, Router,
}; };
use cm_lib::models::Shift;
use diesel::result::OptionalExtension;
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
use diesel_async::{
pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager},
AsyncMysqlConnection, RunQueryDsl,
};
use dotenvy::dotenv;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::SocketAddr; use std::net::SocketAddr;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
include!(concat!(env!("OUT_DIR"), "/templates.rs")); include!(concat!(env!("OUT_DIR"), "/templates.rs"));
async fn establish_connection() -> Pool<AsyncMysqlConnection> {
dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("You must set DATABASE_URL");
let config = AsyncDieselConnectionManager::<AsyncMysqlConnection>::new(database_url);
Pool::builder(config)
.build()
.expect("Error making connection pool")
}
#[derive(Clone)]
pub(crate) struct AppState {
#[allow(dead_code)]
connection: Pool<AsyncMysqlConnection>,
}
impl AppState {
async fn init() -> Self {
Self {
connection: establish_connection().await,
}
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// initialize tracing // initialize tracing
@ -26,10 +61,38 @@ async fn main() {
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.init(); .init();
let state = AppState::init().await;
// let shift = {
// let mut conn = state.connection.get().await.unwrap();
// conn.transaction(|conn| {
// use cm_lib::schema::shifts;
//
// async move {
// diesel::insert_into(shifts::table)
// .default_values()
// .execute(conn)
// .await?;
//
// shifts::table
// .order(shifts::id.desc())
// .select(Shift::as_select())
// .first(conn)
// .await
// }
// .scope_boxed()
// })
// .await
// };
// tracing::debug!("{shift:?}");
// build our application with a route // build our application with a route
let app = Router::new() let app = Router::new()
.nest("/api", api::router())
.route("/", get(root)) .route("/", get(root))
.route("/users", post(create_user)); .route("/users", post(create_user))
.with_state(state);
// run our app with hyper // run our app with hyper
// `axum::Server` is a re-export of `hyper::Server` // `axum::Server` is a re-export of `hyper::Server`
@ -42,8 +105,26 @@ async fn main() {
} }
// basic handler that responds with a static string // basic handler that responds with a static string
async fn root() -> impl IntoResponse { async fn root(State(state): State<AppState>) -> impl IntoResponse {
render!(templates::home_html) let mut conn = state.connection.get().await.unwrap();
let open_shift: Option<Shift> = {
use cm_lib::schema::shifts::dsl::*;
shifts
.filter(end.is_null())
.select(Shift::as_select())
.first(&mut conn)
.await
.optional()
.expect("Query failed")
};
tracing::debug!("{open_shift:?}");
// let is_open_shift = open_shift.is_some();
render!(templates::home_html, open_shift)
} }
async fn create_user( async fn create_user(

View File

@ -19,15 +19,14 @@
<!-- <link rel="icon" href="/icon.svg" type="image/svg+xml"> --> <!-- <link rel="icon" href="/icon.svg" type="image/svg+xml"> -->
<script src="https://unpkg.com/htmx.org@@1.9.5"></script> <script src="https://unpkg.com/htmx.org@@1.9.5"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
<!-- <link rel="manifest" href="site.webmanifest"> --> <!-- <link rel="manifest" href="site.webmanifest"> -->
<meta name="theme-color" content="#fafafa"> <meta name="theme-color" content="#fafafa">
</head> </head>
<body> <body>
<main> @:body()
@:body()
</main>
</body> </body>
</html> </html>

View File

@ -0,0 +1,5 @@
@()
<button hx-post="/api/shifts/close" hx-swap="outerHTML" type="button">
Close Shift
</button>

View File

@ -0,0 +1,5 @@
@()
<button hx-post="/api/shifts/open" hx-swap="outerHTML" type="button">
Open Shift
</button>

View File

@ -1,9 +1,20 @@
@use super::base_html; @use super::base_html;
@use super::components::close_shift_button_html;
@use super::components::open_shift_button_html;
@use cm_lib::models::Shift;
@() @(open_shift: Option<Shift>)
@:base_html({ @:base_html({
<h1>Welcome to clubmanager!</h1> <main>
<div>
@if open_shift.is_none() {
@:open_shift_button_html()
} else {
@:close_shift_button_html()
}
</div>
</main>
}) })