Calendar (WIP)
This commit is contained in:
parent
9d3f9e9c60
commit
79a0aeb1b8
24 changed files with 613 additions and 231 deletions
|
|
@ -16,7 +16,7 @@ tower-http = { version = "0.6", features = ["fs", "trace"] }
|
|||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
chrono = "0.4"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# Rust object notation, to load configuration files.
|
||||
ron = "0.8"
|
||||
|
|
@ -30,8 +30,8 @@ sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
|
|||
rinja = { version = "0.3" }
|
||||
|
||||
argon2 = { version = "0.5", features = ["default", "std"] }
|
||||
rand_core = { version = "0.6", features = ["std"] }
|
||||
rand = "0.8"
|
||||
rand_core = { version = "0.9", features = ["std"] }
|
||||
rand = "0.9"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@
|
|||
width: 14%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
||||
&.current-month {
|
||||
background-color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,6 +172,8 @@ CREATE TABLE [RecipeScheduled] (
|
|||
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX [RecipeScheduled_date_index] ON [RecipeScheduled]([date]);
|
||||
|
||||
CREATE TABLE [ShoppingEntry] (
|
||||
[id] INTEGER PRIMARY KEY,
|
||||
[user_id] INTEGER NOT NULL,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
use chrono::prelude::*;
|
||||
use chrono::{prelude::*, Days};
|
||||
use common::ron_api::Difficulty;
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{Connection, DBError, Result};
|
||||
use crate::data::model;
|
||||
|
||||
use common::ron_api::Difficulty;
|
||||
use crate::{data::model, user_authentication};
|
||||
|
||||
impl Connection {
|
||||
/// Returns all the recipe titles where recipe is written in the given language.
|
||||
|
|
@ -106,11 +105,10 @@ SELECT COUNT(*)
|
|||
FROM [Recipe]
|
||||
INNER JOIN [User] ON [User].[id] = [Recipe].[user_id]
|
||||
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
||||
WHERE [Group].[id] IN ({}) AND ([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2))
|
||||
WHERE [Group].[id] IN ({}) AND ([user_id] = $1 OR (SELECT [is_admin] FROM [User] WHERE [id] = $1))
|
||||
"#,
|
||||
params
|
||||
);
|
||||
|
||||
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
||||
for id in group_ids {
|
||||
query = query.bind(id);
|
||||
|
|
@ -147,11 +145,10 @@ FROM [Recipe]
|
|||
INNER JOIN [User] ON [User].[id] = [Recipe].[user_id]
|
||||
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
||||
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
|
||||
WHERE [Step].[id] IN ({}) AND ([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2))
|
||||
WHERE [Step].[id] IN ({}) AND ([user_id] = $1 OR (SELECT [is_admin] FROM [User] WHERE [id] = $1))
|
||||
"#,
|
||||
params
|
||||
);
|
||||
|
||||
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
||||
for id in steps_ids {
|
||||
query = query.bind(id);
|
||||
|
|
@ -199,11 +196,10 @@ INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|||
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
|
||||
INNER JOIN [Ingredient] ON [Ingredient].[step_id] = [Step].[id]
|
||||
WHERE [Ingredient].[id] IN ({}) AND
|
||||
([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2))
|
||||
([user_id] = $1 OR (SELECT [is_admin] FROM [User] WHERE [id] = $1))
|
||||
"#,
|
||||
params
|
||||
);
|
||||
|
||||
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
||||
for id in ingredients_ids {
|
||||
query = query.bind(id);
|
||||
|
|
@ -755,6 +751,73 @@ VALUES ($1, $2)
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_schedule_recipe(
|
||||
&self,
|
||||
user_id: i64,
|
||||
recipe_id: i64,
|
||||
date: NaiveDate,
|
||||
servings: u32,
|
||||
) -> Result<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO [RecipeScheduled] (user_id, recipe_id, date, servings)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(recipe_id)
|
||||
.bind(date)
|
||||
.bind(servings)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn remove_scheduled_recipe(
|
||||
&self,
|
||||
user_id: i64,
|
||||
recipe_id: i64,
|
||||
date: NaiveDate,
|
||||
) -> Result<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM [RecipeScheduled]
|
||||
WHERE [user_id] = $1 AND [recipe_id] = $2 AND [date] = $3
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(recipe_id)
|
||||
.bind(date)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn get_scheduled_recipes(
|
||||
&self,
|
||||
user_id: i64,
|
||||
start_date: NaiveDate,
|
||||
end_date: NaiveDate,
|
||||
) -> Result<Vec<(NaiveDate, String, i64)>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT [date], [Recipe].[title], [Recipe].[id], [RecipeScheduled].[date]
|
||||
FROM [RecipeScheduled]
|
||||
INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeScheduled].[recipe_id]
|
||||
WHERE [RecipeScheduled].[user_id] = $1 AND [date] >= $2 AND [date] <= $3
|
||||
ORDER BY [date]
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(start_date)
|
||||
.bind(end_date)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -884,4 +947,83 @@ VALUES
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn schedule_recipe() -> Result<()> {
|
||||
let connection = Connection::new_in_memory().await?;
|
||||
let user_id = create_a_user(&connection).await?;
|
||||
|
||||
let recipe_id_1 = connection.create_recipe(user_id).await?;
|
||||
connection.set_recipe_title(recipe_id_1, "recipe 1").await?;
|
||||
|
||||
let recipe_id_2 = connection.create_recipe(user_id).await?;
|
||||
connection.set_recipe_title(recipe_id_2, "recipe 2").await?;
|
||||
|
||||
let today = NaiveDate::from_ymd_opt(2025, 1, 23).unwrap();
|
||||
let yesterday = today - Days::new(1);
|
||||
let tomorrow = today + Days::new(1);
|
||||
|
||||
connection
|
||||
.add_schedule_recipe(user_id, recipe_id_1, today, 4)
|
||||
.await?;
|
||||
connection
|
||||
.add_schedule_recipe(user_id, recipe_id_2, yesterday, 4)
|
||||
.await?;
|
||||
connection
|
||||
.add_schedule_recipe(user_id, recipe_id_1, tomorrow, 4)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
connection
|
||||
.get_scheduled_recipes(user_id, today, today)
|
||||
.await?,
|
||||
vec![(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 23).unwrap(),
|
||||
"recipe 1".to_string(),
|
||||
1
|
||||
)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
connection
|
||||
.get_scheduled_recipes(user_id, yesterday, tomorrow)
|
||||
.await?,
|
||||
vec![
|
||||
(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 22).unwrap(),
|
||||
"recipe 2".to_string(),
|
||||
2
|
||||
),
|
||||
(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 23).unwrap(),
|
||||
"recipe 1".to_string(),
|
||||
1
|
||||
),
|
||||
(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 24).unwrap(),
|
||||
"recipe 1".to_string(),
|
||||
1
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
connection
|
||||
.remove_scheduled_recipe(user_id, recipe_id_1, today)
|
||||
.await?;
|
||||
connection
|
||||
.remove_scheduled_recipe(user_id, recipe_id_2, yesterday)
|
||||
.await?;
|
||||
connection
|
||||
.remove_scheduled_recipe(user_id, recipe_id_1, tomorrow)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
connection
|
||||
.get_scheduled_recipes(user_id, yesterday, tomorrow)
|
||||
.await?,
|
||||
vec![]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use chrono::{prelude::*, Duration};
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use sqlx::Sqlite;
|
||||
|
||||
use super::{Connection, DBError, Result};
|
||||
|
|
@ -57,7 +57,7 @@ pub enum ResetPasswordResult {
|
|||
}
|
||||
|
||||
fn generate_token() -> String {
|
||||
Alphanumeric.sample_string(&mut rand::thread_rng(), consts::TOKEN_SIZE)
|
||||
Alphanumeric.sample_string(&mut rand::rng(), consts::TOKEN_SIZE)
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
|
|
|
|||
|
|
@ -177,6 +177,10 @@ async fn main() {
|
|||
"/recipe/set_ingredients_order",
|
||||
put(services::ron::set_ingredients_order),
|
||||
)
|
||||
.route(
|
||||
"/calendar/get_scheduled_recipes",
|
||||
get(services::ron::get_scheduled_recipes),
|
||||
)
|
||||
.fallback(services::ron::not_found);
|
||||
|
||||
let fragments_routes = Router::new().route(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use axum::{
|
|||
response::{ErrorResponse, IntoResponse, Result},
|
||||
};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||
use chrono::NaiveDate;
|
||||
use serde::Deserialize;
|
||||
// use tracing::{event, Level};
|
||||
|
||||
|
|
@ -183,11 +184,11 @@ async fn check_user_rights_recipe_ingredient(
|
|||
async fn check_user_rights_recipe_ingredients(
|
||||
connection: &db::Connection,
|
||||
user: &Option<model::User>,
|
||||
step_ids: &[i64],
|
||||
ingredient_ids: &[i64],
|
||||
) -> Result<()> {
|
||||
if user.is_none()
|
||||
|| !connection
|
||||
.can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, step_ids)
|
||||
.can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, ingredient_ids)
|
||||
.await?
|
||||
{
|
||||
Err(ErrorResponse::from(ron_error(
|
||||
|
|
@ -599,7 +600,39 @@ pub async fn set_ingredients_order(
|
|||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
///// 404 /////
|
||||
/// Calendar ///
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DateRange {
|
||||
start_date: NaiveDate,
|
||||
end_date: NaiveDate,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn get_scheduled_recipes(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
date_range: Query<DateRange>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
if let Some(user) = user {
|
||||
Ok(ron_response(
|
||||
StatusCode::OK,
|
||||
common::ron_api::ScheduledRecipes {
|
||||
recipes: connection
|
||||
.get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date)
|
||||
.await?,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Err(ErrorResponse::from(ron_error(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
NOT_AUTHORIZED_MESSAGE,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// 404 ///
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
|
||||
ron_error(StatusCode::NOT_FOUND, "Not found")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use std::{borrow::Borrow, fs::File, sync::LazyLock};
|
||||
|
||||
use common::utils;
|
||||
use ron::de::from_reader;
|
||||
use serde::Deserialize;
|
||||
use strum::EnumCount;
|
||||
use strum_macros::EnumCount;
|
||||
use tracing::{event, Level};
|
||||
|
||||
use crate::{consts, utils};
|
||||
use crate::consts;
|
||||
|
||||
#[derive(Debug, Clone, EnumCount, Deserialize)]
|
||||
pub enum Sentence {
|
||||
|
|
@ -109,6 +110,10 @@ pub enum Sentence {
|
|||
RecipeIngredientQuantity,
|
||||
RecipeIngredientUnit,
|
||||
RecipeIngredientComment,
|
||||
RecipeDeleteConfirmation,
|
||||
RecipeGroupDeleteConfirmation,
|
||||
RecipeStepDeleteConfirmation,
|
||||
RecipeIngredientDeleteConfirmation,
|
||||
|
||||
// View Recipe.
|
||||
RecipeOneServing,
|
||||
|
|
|
|||
|
|
@ -39,44 +39,3 @@ pub fn get_url_from_host(host: &str) -> String {
|
|||
host
|
||||
)
|
||||
}
|
||||
|
||||
pub fn substitute(str: &str, pattern: &str, replacements: &[&str]) -> String {
|
||||
let mut result = String::with_capacity(
|
||||
(str.len() + replacements.iter().map(|s| s.len()).sum::<usize>())
|
||||
.saturating_sub(pattern.len() * replacements.len()),
|
||||
);
|
||||
|
||||
let mut i = 0;
|
||||
for s in str.split(pattern) {
|
||||
result.push_str(s);
|
||||
if i < replacements.len() {
|
||||
result.push_str(replacements[i]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if i == 1 {
|
||||
return str.to_string();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_substitute() {
|
||||
assert_eq!(substitute("", "", &[]), "");
|
||||
assert_eq!(substitute("", "", &[""]), "");
|
||||
assert_eq!(substitute("", "{}", &["a"]), "");
|
||||
assert_eq!(substitute("a", "{}", &["b"]), "a");
|
||||
assert_eq!(substitute("a{}", "{}", &["b"]), "ab");
|
||||
assert_eq!(substitute("{}c", "{}", &["b"]), "bc");
|
||||
assert_eq!(substitute("a{}c", "{}", &["b"]), "abc");
|
||||
assert_eq!(substitute("{}b{}", "{}", &["a", "c"]), "abc");
|
||||
assert_eq!(substitute("{}{}{}", "{}", &["a", "bc", "def"]), "abcdef");
|
||||
assert_eq!(substitute("{}{}{}", "{}", &["a"]), "a");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
<ul class="days">
|
||||
{% for i in 0..7 %}
|
||||
{% for j in 0..5 %}
|
||||
<li id="day-{{i}}{{j}}"></li>
|
||||
<li id="day-{{i}}{{j}}"><div class="number"></div><div class="scheduled-recipes"></div></li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -134,6 +134,11 @@
|
|||
</div>
|
||||
|
||||
<div class="dropzone"></div>
|
||||
|
||||
<span class="recipe-delete-confirmation">{{ tr.t(Sentence::RecipeDeleteConfirmation) }}</span>
|
||||
<span class="recipe-group-delete-confirmation">{{ tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span>
|
||||
<span class="recipe-step-delete-confirmation">{{ tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span>
|
||||
<span class="recipe-ingredient-delete-confirmation">{{ tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,10 @@
|
|||
(RecipeIngredientQuantity, "Quantity"),
|
||||
(RecipeIngredientUnit, "Unit"),
|
||||
(RecipeIngredientComment, "Comment"),
|
||||
(RecipeDeleteConfirmation, "Are you sure to delete the recipe: '{}'?"),
|
||||
(RecipeGroupDeleteConfirmation, "Are you sure to delete the group: '{}'?"),
|
||||
(RecipeStepDeleteConfirmation, "Are you sure to delete the step: '{}'?"),
|
||||
(RecipeIngredientDeleteConfirmation, "Are you sure to delete the ingredient: '{}'?"),
|
||||
|
||||
(RecipeOneServing, "1 serving"),
|
||||
(RecipeSomeServings, "{} servings"),
|
||||
|
|
@ -217,6 +221,10 @@
|
|||
(RecipeIngredientQuantity, "Quantité"),
|
||||
(RecipeIngredientUnit, "Unité"),
|
||||
(RecipeIngredientComment, "Commentaire"),
|
||||
(RecipeDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer la recette : '{}' ?"),
|
||||
(RecipeGroupDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer le groupe : '{}' ?"),
|
||||
(RecipeStepDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer l'étape : '{}' ?"),
|
||||
(RecipeIngredientDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer 'ingrédient : '{}' ?"),
|
||||
|
||||
(RecipeOneServing, "pour 1 personne"),
|
||||
(RecipeSomeServings, "pour {} personnes"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue