Calendar is now displayed on home page and recipes can be scheduled without being logged

This commit is contained in:
Greg Burri 2025-02-08 22:31:38 +01:00
parent ccb1248da3
commit 37721ac3ea
22 changed files with 538 additions and 166 deletions

View file

@ -8,7 +8,7 @@ edition = "2021"
common = { path = "../common" }
axum = { version = "0.8", features = ["macros"] }
axum-extra = { version = "0.10", features = ["cookie"] }
axum-extra = { version = "0.10", features = ["cookie", "query"] }
tokio = { version = "1", features = ["full"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["fs", "trace"] }
@ -44,5 +44,5 @@ lettre = { version = "0.11", default-features = false, features = [
"tokio1-rustls-tls",
] }
derive_more = { version = "1", features = ["full"] }
derive_more = { version = "2", features = ["full"] }
thiserror = "2"

View file

@ -55,4 +55,15 @@
}
}
}
}
// Deactivate recipe links in dialog mode.
dialog .calendar .scheduled-recipe {
pointer-events: none;
cursor: text;
text-decoration: none;
}
#hidden-templates-calendar {
display: none;
}

View file

@ -1,8 +1,8 @@
#modal-dialog {
// visibility: hidden;
color: white;
width: 500px;
margin-left: -250px;
width: 800px;
margin-left: -400px;
background-color: black;
text-align: center;
border-radius: 2px;

View file

@ -182,7 +182,7 @@ CREATE INDEX [RecipeScheduled_date_index] ON [RecipeScheduled]([date]);
CREATE TABLE [ShoppingEntry] (
[id] INTEGER PRIMARY KEY,
[user_id] INTEGER NOT NULL,
-- The linkded ingredient can be deleted or a custom entry can be manually added.
-- The linked ingredient can be deleted or a custom entry can be manually added.
-- In both cases [name], [quantity_value] and [quantity_unit] are used to display
-- the entry instead of [Ingredient] data.
[ingredient_id] INTEGER,

View file

@ -1,7 +1,7 @@
use chrono::prelude::*;
use common::ron_api::Difficulty;
use itertools::Itertools;
use sqlx::Error;
use sqlx::{Error, Sqlite};
use super::{Connection, DBError, Result};
use crate::data::model;
@ -64,6 +64,37 @@ ORDER BY [title]
.map_err(DBError::from)
}
/// Returns titles associated to given ids in the same order.
/// Empty string for unknown id.
pub async fn get_recipe_titles(&self, ids: &[i64]) -> Result<Vec<String>> {
let mut query_builder: sqlx::QueryBuilder<Sqlite> =
sqlx::QueryBuilder::new("SELECT [id], [title] FROM [Recipe] WHERE [id] IN(");
let mut separated = query_builder.separated(", ");
for id in ids {
separated.push_bind(id);
}
separated.push_unseparated(")");
let query = query_builder.build_query_as::<(i64, String)>();
let titles = query.fetch_all(&self.pool).await?;
let mut result = vec![];
// Warning: O(n^2), OK for small number of ids.
for id in ids {
result.push(
titles
.iter()
.find_map(|(fetched_id, title)| {
if fetched_id == id {
Some(title.clone())
} else {
None
}
})
.unwrap_or_default(),
);
}
Ok(result)
}
pub async fn can_edit_recipe(&self, user_id: i64, recipe_id: i64) -> Result<bool> {
sqlx::query_scalar(
r#"

View file

@ -107,6 +107,7 @@ async fn main() {
// Disabled: update user profile is now made with a post data ('edit_user_post').
// .route("/user/update", put(services::ron::update_user))
.route("/set_lang", put(services::ron::set_lang))
.route("/recipe/get_titles", get(services::ron::get_titles))
.route("/recipe/set_title", put(services::ron::set_recipe_title))
.route(
"/recipe/set_description",

View file

@ -1,12 +1,14 @@
use axum::{
debug_handler,
extract::{Extension, Query, State},
extract::{Extension, State},
http::{HeaderMap, StatusCode},
response::{ErrorResponse, IntoResponse, Response, Result},
};
use axum_extra::extract::cookie::{Cookie, CookieJar};
use chrono::NaiveDate;
use serde::Deserialize;
use axum_extra::extract::{
cookie::{Cookie, CookieJar},
Query,
};
use serde::{Deserialize, Serialize};
// use tracing::{event, Level};
use crate::{
@ -20,11 +22,15 @@ use crate::{
const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized";
#[derive(Deserialize)]
pub struct RecipeId {
#[serde(rename = "recipe_id")]
pub struct Id {
id: i64,
}
#[derive(Deserialize, Serialize)]
pub struct Ids {
ids: Vec<i64>,
}
// #[allow(dead_code)]
// #[debug_handler]
// pub async fn update_user(
@ -67,6 +73,8 @@ pub async fn set_lang(
Ok((jar, StatusCode::OK))
}
/*** Rights ***/
async fn check_user_rights_recipe(
connection: &db::Connection,
user: &Option<model::User>,
@ -200,6 +208,20 @@ async fn check_user_rights_recipe_ingredients(
}
}
/*** Recipe ***/
/// Ask recipe titles associated with each given id. The returned titles are in the same order
/// as the given ids.
#[debug_handler]
pub async fn get_titles(
State(connection): State<db::Connection>,
recipe_ids: Query<Ids>,
) -> Result<impl IntoResponse> {
Ok(ron_response_ok(common::ron_api::Strings {
strs: connection.get_recipe_titles(&recipe_ids.ids).await?,
}))
}
#[debug_handler]
pub async fn set_recipe_title(
State(connection): State<db::Connection>,
@ -255,7 +277,7 @@ pub async fn set_estimated_time(
#[debug_handler]
pub async fn get_tags(
State(connection): State<db::Connection>,
recipe_id: Query<RecipeId>,
recipe_id: Query<Id>,
) -> Result<impl IntoResponse> {
Ok(ron_response_ok(common::ron_api::Tags {
recipe_id: recipe_id.id,
@ -395,7 +417,7 @@ impl From<model::Ingredient> for common::ron_api::Ingredient {
#[debug_handler]
pub async fn get_groups(
State(connection): State<db::Connection>,
recipe_id: Query<RecipeId>,
recipe_id: Query<Id>,
) -> Result<impl IntoResponse> {
// Here we don't check user rights on purpose.
Ok(ron_response_ok(
@ -598,17 +620,11 @@ pub async fn set_ingredients_order(
/// 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>,
date_range: Query<common::ron_api::DateRange>,
) -> Result<impl IntoResponse> {
if let Some(user) = user {
Ok(ron_response_ok(common::ron_api::ScheduledRecipes {

View file

@ -3,14 +3,14 @@ use std::{collections::HashMap, net::SocketAddr};
use axum::{
body::Body,
debug_handler,
extract::{ConnectInfo, Extension, Query, Request, State},
extract::{ConnectInfo, Extension, Request, State},
http::HeaderMap,
response::{Html, IntoResponse, Redirect, Response},
Form,
};
use axum_extra::extract::{
cookie::{Cookie, CookieJar},
Host,
Host, Query,
};
use chrono::Duration;
use lettre::Address;

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ tr.current_lang_and_territory_code() }}">
<html lang="{{ tr.current_lang_and_territory_code() }}" data-user-logged="{{ user.is_some() }}" >
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

View file

@ -45,4 +45,8 @@
{% endfor %}
{% endfor %}
</ul>
<div id="hidden-templates-calendar">
<div class="scheduled-recipe"><a></a><span class="remove-scheduled-recipe">X</span></div>
</div>
</div>

View file

@ -3,7 +3,7 @@
{% block content %}
<div class="content" id="home">
HOME: TODO
{% include "calendar.html" %}
</div>
{% endblock %}

View file

@ -9,9 +9,10 @@
{% if crate::data::model::can_user_edit_recipe(user, recipe) %}
<a class="edit-recipe" href="/recipe/edit/{{ recipe.id }}" >Edit</a>
{% endif %}
<span class="add-to-planner">{{ tr.t(Sentence::CalendarAddToPlanner) }}</span>
{% endif %}
<span class="add-to-planner">{{ tr.t(Sentence::CalendarAddToPlanner) }}</span>
<div class="tags">
{% for tag in recipe.tags %}
<span class="tag">{{ tag }}</span>
@ -81,17 +82,21 @@
<div id="hidden-templates">
{# To create a modal dialog to choose a date and and servings #}
{% if let Some(user) = user %}
<div class="date-and-servings" >
{% include "calendar.html" %}
<label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label>
<input
id="input-servings"
type="number"
step="1" min="1" max="100"
value="{{ user.default_servings }}"/>
</div>
{% endif %}
<div class="date-and-servings" >
{% include "calendar.html" %}
<label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label>
<input
id="input-servings"
type="number"
step="1" min="1" max="100"
value="
{% if let Some(user) = user %}
{{ user.default_servings }}
{% else %}
4
{% endif %}
"/>
</div>
<span class="calendar-add-to-planner-success">{{ tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span>
<span class="calendar-add-to-planner-already-exists">{{ tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span>