Set user language to new recipe + cleaning

This commit is contained in:
Greg Burri 2025-01-06 22:52:22 +01:00
parent 8b4b788562
commit 03ebbb74fa
12 changed files with 73 additions and 86 deletions

View file

@ -1,4 +1,4 @@
use std::{sync::LazyLock, time::Duration}; use std::time::Duration;
pub const FILE_CONF: &str = "conf.ron"; pub const FILE_CONF: &str = "conf.ron";
pub const TRANSLATION_FILE: &str = "translation.ron"; pub const TRANSLATION_FILE: &str = "translation.ron";
@ -23,10 +23,3 @@ pub const SEND_EMAIL_TIMEOUT: Duration = Duration::from_secs(60);
pub const REVERSE_PROXY_IP_HTTP_FIELD: &str = "x-real-ip"; // Set by the reverse proxy (Nginx). pub const REVERSE_PROXY_IP_HTTP_FIELD: &str = "x-real-ip"; // Set by the reverse proxy (Nginx).
pub const MAX_DB_CONNECTION: u32 = 1; // To avoid database lock. pub const MAX_DB_CONNECTION: u32 = 1; // To avoid database lock.
// TODO: remove, should be replaced by the translation module.
pub static LANGUAGES: LazyLock<[(&str, &str); 2]> = LazyLock::new(|| {
let mut langs = [("Français", "fr"), ("English", "en")];
langs.sort();
langs
});

View file

@ -32,9 +32,6 @@ pub enum DBError {
)] )]
UnsupportedVersion(u32), UnsupportedVersion(u32),
#[error("Unknown language: {0}")]
UnknownLanguage(String),
#[error("Unknown error: {0}")] #[error("Unknown error: {0}")]
Other(String), Other(String),
} }

View file

@ -1,18 +1,38 @@
use super::{Connection, DBError, Result}; use super::{Connection, DBError, Result};
use crate::{consts, data::model}; use crate::data::model;
use common::ron_api::Difficulty; use common::ron_api::Difficulty;
impl Connection { impl Connection {
pub async fn get_all_published_recipe_titles(&self) -> Result<Vec<(i64, String)>> { /// Returns all the recipe titles where recipe is written in the given language.
/// If a user_id is given, the language constraint is ignored for recipes owned by user_id.
pub async fn get_all_published_recipe_titles(
&self,
lang: &str,
user_id: Option<i64>,
) -> Result<Vec<(i64, String)>> {
if let Some(user_id) = user_id {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT [id], [title] SELECT [id], [title]
FROM [Recipe] FROM [Recipe]
WHERE [is_published] = true WHERE [is_published] = true AND ([lang] = $1 OR [user_id] = $2)
ORDER BY [title] ORDER BY [title]
"#, "#,
) )
.bind(lang)
.bind(user_id)
} else {
sqlx::query_as(
r#"
SELECT [id], [title]
FROM [Recipe]
WHERE [is_published] = true AND [lang] = $1
ORDER BY [title]
"#,
)
.bind(lang)
}
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map_err(DBError::from) .map_err(DBError::from)
@ -147,9 +167,16 @@ WHERE [Recipe].[user_id] = $1
{ {
Some(recipe_id) => Ok(recipe_id), Some(recipe_id) => Ok(recipe_id),
None => { None => {
let db_result = let lang: String = sqlx::query_scalar("SELECT [lang] FROM [User] WHERE [id] = $1")
sqlx::query("INSERT INTO [Recipe] ([user_id], [title]) VALUES ($1, '')")
.bind(user_id) .bind(user_id)
.fetch_one(&mut *tx)
.await?;
let db_result = sqlx::query(
"INSERT INTO [Recipe] ([user_id], [lang], [title]) VALUES ($1, $2, '')",
)
.bind(user_id)
.bind(lang)
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
@ -345,9 +372,6 @@ WHERE [id] = $1 AND [id] NOT IN (
} }
pub async fn set_recipe_language(&self, recipe_id: i64, lang: &str) -> Result<()> { pub async fn set_recipe_language(&self, recipe_id: i64, lang: &str) -> Result<()> {
if !consts::LANGUAGES.iter().any(|(_, l)| *l == lang) {
return Err(DBError::UnknownLanguage(lang.to_string()));
}
sqlx::query("UPDATE [Recipe] SET [lang] = $2 WHERE [id] = $1") sqlx::query("UPDATE [Recipe] SET [lang] = $2 WHERE [id] = $1")
.bind(recipe_id) .bind(recipe_id)
.bind(lang) .bind(lang)
@ -598,24 +622,6 @@ mod tests {
Ok(()) Ok(())
} }
#[tokio::test]
async fn set_nonexistent_language() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let user_id = create_a_user(&connection).await?;
let recipe_id = connection.create_recipe(user_id).await?;
match connection.set_recipe_language(recipe_id, "asdf").await {
// Nominal case.
Err(DBError::UnknownLanguage(message)) => {
println!("Ok: {}", message);
}
other => panic!("Set an nonexistent language must fail: {:?}", other),
}
Ok(())
}
async fn create_a_user(connection: &Connection) -> Result<i64> { async fn create_a_user(connection: &Connection) -> Result<i64> {
let user_id = 1; let user_id = 1;
connection.execute_sql( connection.execute_sql(

View file

@ -133,13 +133,11 @@ pub struct RecipeEditTemplate {
pub recipes: Recipes, pub recipes: Recipes,
pub recipe: model::Recipe, pub recipe: model::Recipe,
pub languages: [(&'static str, &'static str); 2],
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "recipes_list_fragment.html")] #[template(path = "recipes_list_fragment.html")]
pub struct RecipesListFragmentTemplate { pub struct RecipesListFragmentTemplate {
pub user: Option<model::User>,
pub tr: Tr, pub tr: Tr,
pub recipes: Recipes, pub recipes: Recipes,

View file

@ -5,7 +5,6 @@ use axum::{
middleware::Next, middleware::Next,
response::{IntoResponse, Response, Result}, response::{IntoResponse, Response, Result},
}; };
// use tracing::{event, Level};
use crate::{ use crate::{
data::{db, model}, data::{db, model},
@ -54,7 +53,9 @@ pub async fn home_page(
Extension(tr): Extension<translation::Tr>, Extension(tr): Extension<translation::Tr>,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
let recipes = Recipes { let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?, published: connection
.get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
.await?,
unpublished: if let Some(user) = user.as_ref() { unpublished: if let Some(user) = user.as_ref() {
connection connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)

View file

@ -25,7 +25,9 @@ pub async fn recipes_list_fragments(
Extension(tr): Extension<translation::Tr>, Extension(tr): Extension<translation::Tr>,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
let recipes = Recipes { let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?, published: connection
.get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
.await?,
unpublished: if let Some(user) = user.as_ref() { unpublished: if let Some(user) = user.as_ref() {
connection connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)
@ -35,5 +37,5 @@ pub async fn recipes_list_fragments(
}, },
current_id: current_recipe.current_recipe_id, current_id: current_recipe.current_recipe_id,
}; };
Ok(RecipesListFragmentTemplate { user, tr, recipes }) Ok(RecipesListFragmentTemplate { tr, recipes })
} }

View file

@ -6,7 +6,6 @@ use axum::{
// use tracing::{event, Level}; // use tracing::{event, Level};
use crate::{ use crate::{
consts,
data::{db, model}, data::{db, model},
html_templates::*, html_templates::*,
translation::{self, Sentence}, translation::{self, Sentence},
@ -37,21 +36,20 @@ pub async fn edit_recipe(
if let Some(recipe) = connection.get_recipe(recipe_id, false).await? { if let Some(recipe) = connection.get_recipe(recipe_id, false).await? {
if recipe.user_id == user.id { if recipe.user_id == user.id {
let recipes = Recipes { let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?, published: connection
.get_all_published_recipe_titles(tr.current_lang_code(), Some(user.id))
.await?,
unpublished: connection unpublished: connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)
.await?, .await?,
current_id: Some(recipe_id), current_id: Some(recipe_id),
}; };
dbg!(&recipe);
Ok(RecipeEditTemplate { Ok(RecipeEditTemplate {
user: Some(user), user: Some(user),
tr, tr,
recipes, recipes,
recipe, recipe,
languages: *consts::LANGUAGES,
} }
.into_response()) .into_response())
} else { } else {
@ -89,7 +87,12 @@ pub async fn view(
} }
let recipes = Recipes { let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?, published: connection
.get_all_published_recipe_titles(
tr.current_lang_code(),
user.as_ref().map(|u| u.id),
)
.await?,
unpublished: if let Some(user) = user.as_ref() { unpublished: if let Some(user) = user.as_ref() {
connection connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)

View file

@ -249,6 +249,14 @@ pub async fn set_language(
Extension(user): Extension<Option<model::User>>, Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeLanguage>, ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeLanguage>,
) -> Result<StatusCode> { ) -> Result<StatusCode> {
if !crate::translation::Tr::available_codes()
.iter()
.any(|&l| l == ron.lang)
{
// TODO: log?
return Ok(StatusCode::BAD_REQUEST);
}
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
connection connection
.set_recipe_language(ron.recipe_id, &ron.lang) .set_recipe_language(ron.recipe_id, &ron.lang)

View file

@ -59,12 +59,12 @@
<label for="select-language">{{ tr.t(Sentence::RecipeLanguage) }}</label> <label for="select-language">{{ tr.t(Sentence::RecipeLanguage) }}</label>
<select id="select-language"> <select id="select-language">
{% for lang in languages %} {% for lang in Tr::available_languages() %}
<option value="{{ lang.1 }}" <option value="{{ lang.0 }}"
{%+ if recipe.lang == lang.1 %} {%+ if recipe.lang == lang.0 %}
selected selected
{% endif %} {% endif %}
>{{ lang.0 }}</option> >{{ lang.1 }}</option>
{% endfor %} {% endfor %}
</select> </select>

View file

@ -1,5 +1,4 @@
use gloo::{ use gloo::{
console::log,
events::EventListener, events::EventListener,
net::http::Request, net::http::Request,
utils::{document, window}, utils::{document, window},

View file

@ -5,11 +5,7 @@ mod request;
mod toast; mod toast;
mod utils; mod utils;
use gloo::{ use gloo::{events::EventListener, utils::window};
console::log,
events::EventListener,
utils::{document, window},
};
use utils::by_id; use utils::by_id;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
@ -17,24 +13,10 @@ use web_sys::HtmlSelectElement;
use common::ron_api; use common::ron_api;
// #[wasm_bindgen]
// extern "C" {
// fn alert(s: &str);
// }
// #[wasm_bindgen]
// pub fn greet(name: &str) {
// alert(&format!("Hello, {}!", name));
// console::log_1(&"Hello bg".into());
// }
#[wasm_bindgen(start)] #[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> { pub fn main() -> Result<(), JsValue> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
// let window = web_sys::window().expect("no global `window` exists");
// let document = window.document().expect("should have a document on window");
let location = window().location().pathname()?; let location = window().location().pathname()?;
let path: Vec<&str> = location.split('/').skip(1).collect(); let path: Vec<&str> = location.split('/').skip(1).collect();
@ -56,8 +38,6 @@ pub fn main() -> Result<(), JsValue> {
let _ = request::put::<(), _>("set_lang", body).await; let _ = request::put::<(), _>("set_lang", body).await;
let _ = window().location().reload(); let _ = window().location().reload();
}); });
// log!(lang);
}) })
.forget(); .forget();

View file

@ -1,6 +1,6 @@
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::stream::Stream; use futures::stream::Stream;
use gloo::{console::log, events::EventListener, net::http::Request, utils::document}; use gloo::events::EventListener;
use std::{ use std::{
future::Future, future::Future,
pin::Pin, pin::Pin,