Skip to content

Commit

Permalink
refa: simplifying get expenses, user now inputs min and max bounds
Browse files Browse the repository at this point in the history
directly on datetimes, instead of selecting a month and us converting it
for them
  • Loading branch information
nicolasauler committed Sep 29, 2024
1 parent 263cb62 commit e7b722f
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 129 deletions.
38 changes: 18 additions & 20 deletions src/data_structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,30 +61,28 @@ impl Display for Months {
}
}

impl TryInto<u32> for Months {
type Error = &'static str;

fn try_into(self) -> Result<u32, Self::Error> {
match self {
Self::January => Ok(1),
Self::February => Ok(2),
Self::March => Ok(3),
Self::April => Ok(4),
Self::May => Ok(5),
Self::June => Ok(6),
Self::July => Ok(7),
Self::August => Ok(8),
Self::September => Ok(9),
Self::October => Ok(10),
Self::November => Ok(11),
Self::December => Ok(12),
impl From<Months> for u32 {
fn from(value: Months) -> Self {
match value {
Months::January => 1,
Months::February => 2,
Months::March => 3,
Months::April => 4,
Months::May => 5,
Months::June => 6,
Months::July => 7,
Months::August => 8,
Months::September => 9,
Months::October => 10,
Months::November => 11,
Months::December => 12,
}
}
}

impl Months {
pub const fn from_chrono_month(month: Month) -> Self {
match month {
impl From<chrono::Month> for Months {
fn from(value: chrono::Month) -> Self {
match value {
Month::January => Self::January,
Month::February => Self::February,
Month::March => Self::March,
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod templates;
/// Module containing time and crypto utility functions.
mod util;

use crate::{auth::Backend, data_structs::Months};
use crate::auth::Backend;
use std::{net::SocketAddr, sync::Arc, time::Duration};

use anyhow::bail;
Expand Down
37 changes: 24 additions & 13 deletions src/schema.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::Months;
use std::{
fmt::{self, Display},
str::FromStr,
Expand Down Expand Up @@ -66,16 +65,17 @@ pub struct Expense {

#[derive(Deserialize, Debug)]
/// `GetExpense` is a struct with the fields of an expense that can be retrieved.
/// Currently, only the month is supported and it is optional.
/// If no month is passed, all expenses are retrieved.
pub struct GetExpense {
#[serde(deserialize_with = "empty_string_to_none")]
pub month: Option<Months>,
pub from: Option<NaiveDate>,
#[serde(deserialize_with = "empty_string_to_none")]
pub to: Option<NaiveDate>,
}

#[derive(Deserialize, Debug)]
/// `UpdateExpense` is a struct with the fields of an expense that can be updated.
/// All fields are optional.
/// This is part of the contract of the Data API.
pub struct UpdateExpenseApi {
pub description: Option<String>,
pub price: Option<f32>,
Expand All @@ -84,10 +84,20 @@ pub struct UpdateExpenseApi {
pub date: Option<NaiveDate>,
}

#[derive(Deserialize, Debug, Default)]
#[derive(Deserialize, Debug)]
pub struct CreateExpense {
pub description: String,
pub price: f32,
pub category: ExpenseCategory,
pub is_essential: bool,
pub date: NaiveDate,
}

#[derive(Deserialize, Debug)]
/// `UpdateExpense` is a struct with the fields of an expense that can be updated.
/// All fields are optional.
pub struct UpdateExpense {
/// This is part of the contract of the Hypermedia API.
pub struct UpdateExpenseHypr {
pub description: Option<String>,
#[serde(deserialize_with = "de_string_to_option_f32")]
pub price: Option<f32>,
Expand All @@ -101,11 +111,10 @@ pub struct UpdateExpense {
}

/// Function to set the default value for `is_essential` in `UpdateExpense` to be Some(false).
#[allow(clippy::unnecessary_wraps)]
//#[allow(
// clippy::unnecessary_wraps,
// reason = "Needs to return option for custom deserializer"
//)]
#[expect(
clippy::unnecessary_wraps,
reason = "Needs to return option for custom deserializer"
)]
const fn default_is_essential_to_false() -> Option<bool> {
return Some(false);
}
Expand All @@ -130,15 +139,17 @@ where
/// If the string is empty, returns None.
/// If the string is a valid month, returns Some(Months).
/// If the string is not a valid month, returns an error.
fn empty_string_to_none<'de, D>(deserializer: D) -> Result<Option<Months>, D::Error>
fn empty_string_to_none<'de, D>(deserializer: D) -> Result<Option<NaiveDate>, D::Error>
where
D: serde::Deserializer<'de>,
{
let str: String = Deserialize::deserialize(deserializer)?;
if str.is_empty() {
return Ok(None);
}
return Ok(Some(Months::from_str(&str).map_err(de::Error::custom)?));
return Ok(Some(
NaiveDate::parse_from_str(&str, "%Y-%m-%d").map_err(de::Error::custom)?,
));
}

/// Deserialization serde function that parses a f32 from a string.
Expand Down
21 changes: 1 addition & 20 deletions src/templates.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
use crate::{
data_structs::{Months, MonthsIter},
schema::{ExpenseCategory, ExpenseCategoryIter},
util::{add_csp_to_response, generate_otp_token},
};

use askama_axum::{IntoResponse, Template};
use axum::{body::Body, http::Response};
use chrono::{Datelike, Month, NaiveDate, Utc};
use chrono::NaiveDate;
use strum::IntoEnumIterator;
use uuid::Uuid;

#[derive(Template)]
#[template(path = "expenses.html")]
/// The askama template for the expenses page.
pub struct ExpensesTemplate {
/// The current month to be displayed in English in the dropdown.
pub current_month: Months,
/// The expense types to be displayed in the dropdown.
pub expense_categories: ExpenseCategoryIter,
/// The months to be displayed in the dropdown.
pub months: MonthsIter,
/// The username of the logged in user.
pub username: String,
/// CSP nonce
Expand All @@ -29,19 +24,7 @@ pub struct ExpensesTemplate {
impl Default for ExpensesTemplate {
fn default() -> Self {
return Self {
current_month: {
Months::from_chrono_month(
Month::try_from(u8::try_from(Utc::now().month()).unwrap_or_else(|_| {
tracing::error!(
"Failed to convert chrono month to u8, defaulting to 1(January)"
);
return 1;
}))
.unwrap_or(Month::January),
)
},
expense_categories: ExpenseCategory::iter(),
months: Months::iter(),
username: String::new(),
nonce: String::new(),
};
Expand All @@ -56,9 +39,7 @@ impl ExpensesTemplate {
let nonce_str = format!("'nonce-{nonce}'");

let mut response = Self {
current_month: self.current_month,
expense_categories: self.expense_categories,
months: self.months,
username: self.username,
nonce,
}
Expand Down
60 changes: 0 additions & 60 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -1,67 +1,7 @@
use crate::data_structs::Months;

use axum::{body::Body, http::Response};
use axum_helmet::ContentSecurityPolicy;
use chrono::{Datelike, NaiveDate, Utc};
use rand::{distributions::Alphanumeric, thread_rng, Rng};

/// Returns the first day of a month passed as an unsigned integer as a `NaiveDate`.
/// Example: `get_first_day_from_month(1)` returns 2023-01-01.
pub fn get_first_day_from_month(month: u32) -> Option<NaiveDate> {
return Utc::now()
.with_day(1)
.and_then(|date| return date.naive_utc().date().with_month(month));
}

/// If a month is passed, as a Some(Months), returns the first day of that month as a `NaiveDate`.
/// If None is passed, returns None.
/// Months is an enum with the months of the year.
pub fn get_first_day_from_month_or_none(
option_month: Option<Months>,
) -> Result<Option<NaiveDate>, &'static str> {
let Some(month) = option_month else {
return Ok(None);
};
let month_n: u32 = month
.try_into()
.map_err(|_ignore| return "Could not convert month to u32.")?;
match get_first_day_from_month(month_n) {
Some(date) => return Ok(Some(date)),
None => return Err("Could not get first day from month."),
}
}

/// Returns the last day of a month passed as an unsigned integer as a `NaiveDate`.
/// Example: `get_last_day_from_month(1)` returns 2023-01-31.
pub fn get_last_day_from_month(month: u32) -> Option<NaiveDate> {
let first_day_of_month = get_first_day_from_month(month)?;

if first_day_of_month.month() == 12 {
return first_day_of_month.with_day(31);
}
return first_day_of_month
.with_month(month.checked_add(1)?)
.and_then(|date| return date.checked_sub_days(chrono::Days::new(1)));
}

/// If a month is passed, as a Some(Months), returns the last day of that month as a `NaiveDate`.
/// If None is passed, returns None.
/// Months is an enum with the months of the year.
pub fn get_last_day_from_month_or_none(
option_month: Option<Months>,
) -> Result<Option<NaiveDate>, &'static str> {
let Some(month) = option_month else {
return Ok(None);
};
let month_n: u32 = month
.try_into()
.map_err(|_ignore| return "Could not convert month to u32.")?;
match get_last_day_from_month(month_n) {
Some(date) => return Ok(Some(date)),
None => return Err("Could not get last day from month, maybe it underflowed?"),
}
}

/// Generates a random string of 128 characters to be used as an email verification token.
pub fn generate_verification_token() -> String {
return thread_rng()
Expand Down
27 changes: 12 additions & 15 deletions templates/expenses.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,24 @@ <h3>{{ username }}'s Expenses</h3>
</div>
</section>

<form hx-get="/expenses" hx-target="#expenses-table-body" hx-trigger="load, refresh-table from:body, change from:#month">
<form hx-get="/expenses" hx-target="#expenses-table-body" hx-trigger="load, refresh-table from:body, change from:#from, change from:#to">
<div class="grid">
<select name="month" id="month">
{% for month in months.clone() %}
{% if month == current_month %}
<option value="{{ month }}" selected>{{ month }}</option>
{% else %}
<option value="{{ month }}">{{ month }}</option>
{% endif %}
{% endfor %}
<option value="">Entire year</option>
</select>
<label for="from-date">From Date
<input type="date" name="from" id="from">
</label>

<label for="to-date">To Date
<input type="date" name="to" id="to">
</label>

<button _="on click toggle @open on #modal-example" class="contrast" type="button">Add Expense</button>
</div>
</form>

<div hx-get="/expenses/plots"
hx-target="#expenses-plots"
hx-trigger="load, change from:#month, refresh-table from:body, refresh-plots from:body"
hx-include="#month"
hx-trigger="load, change from:#to, change from:#from, refresh-table from:body, refresh-plots from:body"
hx-include="#from, #to"
>
</div>

Expand Down Expand Up @@ -76,8 +73,8 @@ <h3>{{ username }}'s Expenses</h3>

<div hx-get="/expenses/plots"
hx-target="#expenses-plots"
hx-trigger="load, change from:#month, refresh-table from:body, refresh-plots from:body"
hx-include="#month"
hx-trigger="load, change from:#from, change from:#to, refresh-table from:body, refresh-plots from:body"
hx-include="#from, #to"
>
</div>

Expand Down

0 comments on commit e7b722f

Please sign in to comment.