Calendar is now displayed on home page and recipes can be scheduled without being logged
This commit is contained in:
parent
ccb1248da3
commit
37721ac3ea
22 changed files with 538 additions and 166 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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#"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
{% block content %}
|
||||
|
||||
<div class="content" id="home">
|
||||
HOME: TODO
|
||||
{% include "calendar.html" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue