2m2d/src/tasks.rs

232 lines
5.8 KiB
Rust

use crate::ctx;
use crate::login_or_redirect;
use crate::Error;
use axum::extract::Extension;
use axum::extract::Form;
use axum::extract::OriginalUri;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::response::Html;
use axum::response::Redirect;
use axum_sessions::extractors::ReadableSession;
use num_enum::FromPrimitive;
use serde::Deserialize;
use serde::Serialize;
use sqlx::FromRow;
use sqlx::{Pool, Postgres};
use std::sync::Arc;
use tera::Tera;
use tracing::info;
#[derive(Deserialize)]
pub struct BareTask {
pub title: String,
pub description: String,
pub status: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct RawTask {
pub id: i32,
pub title: String,
pub description: String,
pub status: i32,
}
#[derive(FromPrimitive, Serialize, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)]
#[repr(i32)]
pub enum Status {
#[default]
#[serde(rename = "To Do")]
ToDo,
#[serde(rename = "In Progress")]
InProgress,
Done,
}
#[derive(Serialize, Ord, Eq, Clone)]
pub struct Task {
pub id: i32,
pub title: String,
pub description: String,
pub status: Status,
}
impl From<RawTask> for Task {
fn from(t: RawTask) -> Self {
Self {
id: t.id,
title: t.title,
description: t.description,
status: Status::from(t.status),
}
}
}
impl RawTask {
#[allow(dead_code)]
fn from_bare(t: BareTask, id: i32) -> Self {
Self {
id,
title: t.title,
description: t.description,
status: t.status,
}
}
}
impl Task {
#[allow(dead_code)]
fn from_bare(t: BareTask, id: i32) -> Self {
RawTask::from_bare(t, id).into()
}
}
impl std::cmp::PartialEq<Task> for Task {
fn eq(&self, other: &Task) -> bool {
self.id == other.id
}
}
impl std::cmp::PartialOrd<Task> for Task {
fn partial_cmp(&self, other: &Task) -> Option<std::cmp::Ordering> {
self.id.partial_cmp(&other.id)
}
}
pub async fn update_form(
Path(id): Path<i32>,
Extension(tera): Extension<Arc<Tera>>,
Extension(pool): Extension<Arc<Pool<Postgres>>>,
session: ReadableSession,
OriginalUri(uri): OriginalUri,
) -> Result<Html<String>, Error> {
let username = login_or_redirect!(session, "/login");
info!("Getting user id");
let (user_id,): (i32,) = sqlx::query_as("select id from users where username=$1")
.bind(&username)
.fetch_one(&*pool)
.await?;
info!("Getting task");
let task: Option<RawTask> =
sqlx::query_as("select id,title,description,status from tasks where id=$1 and owner=$2")
.bind(id)
.bind(user_id)
.fetch_optional(&*pool)
.await?;
if let Some(task) = task {
info!("Rendering template");
let c = ctx! {
"task" => task,
"url" => uri.to_string()
};
let rendered = tera.render("tasks/update.html", &c)?;
info!("Rendering finished, returning");
#[allow(clippy::needless_return)] // it's a compile error, clippy
return Ok(Html(rendered));
}
Err(StatusCode::NOT_FOUND.into())
}
pub async fn create_form(
Extension(tera): Extension<Arc<Tera>>,
OriginalUri(uri): OriginalUri,
session: ReadableSession,
) -> Result<Html<String>, Error> {
let _username = login_or_redirect!(session, "/login");
let c = ctx! {
"url" => uri.to_string()
};
let rendered = tera.render("tasks/create.html", &c)?;
Ok(Html(rendered))
}
pub async fn update_backend(
Path(id): Path<i32>,
Extension(pool): Extension<Arc<Pool<Postgres>>>,
Form(data): Form<BareTask>,
) -> Result<Redirect, Error> {
sqlx::query("update tasks set title=$1,description=$2,status=$3 where id=$4")
.bind(&data.title)
.bind(&data.description)
.bind(data.status)
.bind(id)
.execute(&*pool)
.await?;
Ok(Redirect::to(&format!("/tasks/{}", id)))
}
pub async fn create_backend(
Extension(pool): Extension<Arc<Pool<Postgres>>>,
Form(data): Form<BareTask>,
session: ReadableSession,
) -> Result<Redirect, Error> {
let username = login_or_redirect!(session, "/login");
info!("Getting user id");
let (user_id,): (i32,) = sqlx::query_as("select id from users where username=$1")
.bind(&username)
.fetch_one(&*pool)
.await?;
info!("Inserting task...");
let (id,): (i32,) = sqlx::query_as(
"insert into tasks (owner,title, description, status) values ($1, $2, $3, $4) returning id",
)
.bind(user_id)
.bind(&data.title)
.bind(&data.description)
.bind(data.status)
.fetch_one(&*pool)
.await?;
info!("Done, redirecting");
Ok(Redirect::to(&format!("/tasks/{}", id)))
}
pub async fn task_detail(
Path(id): Path<i32>,
Extension(tera): Extension<Arc<Tera>>,
Extension(pool): Extension<Arc<Pool<Postgres>>>,
session: ReadableSession,
) -> Result<Html<String>, Error> {
let username = login_or_redirect!(session, "/login");
info!("Getting user id");
let (user_id,): (i32,) = sqlx::query_as("select id from users where username=$1")
.bind(&username)
.fetch_one(&*pool)
.await?;
info!("Getting task");
let task: Option<RawTask> =
sqlx::query_as("select id,title,description,status from tasks where id=$1 and owner=$2")
.bind(id)
.bind(user_id)
.fetch_optional(&*pool)
.await?;
if let Some(task) = task.map(Task::from) {
info!("Rendering");
let c = ctx! {
"task" => task
};
let rendered = tera.render("tasks/detail.html", &c)?;
info!("Rendering complete, returning");
return Ok(Html(rendered));
}
Err(StatusCode::NOT_FOUND.into())
}