diff --git a/crates/app/storyteller_web/src/database/query_builders/list_categories_query_builder.rs b/crates/app/storyteller_web/src/database/query_builders/list_categories_query_builder.rs index ebf79ecc22..784247c5c3 100644 --- a/crates/app/storyteller_web/src/database/query_builders/list_categories_query_builder.rs +++ b/crates/app/storyteller_web/src/database/query_builders/list_categories_query_builder.rs @@ -1,6 +1,5 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; -use config::shared_constants::DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE; use crate::database::enums::record_visibility::RecordVisibility; use crate::database::helpers::boolean_converters::{i8_to_bool, nullable_i8_to_optional_bool}; use crate::util::anyhow_result::AnyhowResult; @@ -97,7 +96,7 @@ impl ListCategoriesQueryBuilder { } /// Perform the query based on the set predicates. - pub async fn perform_query_for_page( + pub async fn perform_query( &self, mysql_pool: &MySqlPool ) -> AnyhowResult { @@ -134,7 +133,7 @@ impl ListCategoriesQueryBuilder { } /// Perform the query based on the set predicates. - pub async fn perform_internal_query( + async fn perform_internal_query( &self, mysql_pool: &MySqlPool ) -> AnyhowResult> { @@ -158,7 +157,7 @@ impl ListCategoriesQueryBuilder { Ok(results) } - pub fn build_query_string(&self) -> String { + fn build_query_string(&self) -> String { // TODO: I haven't figured out how to get field name disambiguation and type coercion working here. // (1) tts_results.creator_set_visibility `creator_set_visibility: crate::database::enums::record_visibility::RecordVisibility`, // Query error: no column found for name: creator_set_visibility @@ -168,7 +167,7 @@ impl ListCategoriesQueryBuilder { // TODO/NB: Unfortunately SQLx can't statically typecheck this query let mut query = r#" SELECT - model_categories.token as model_token, + model_categories.token as category_token, model_categories.model_type, @@ -235,10 +234,10 @@ LEFT OUTER JOIN users if !self.show_deleted { if !first_predicate_added { - query.push_str(" WHERE model_categories.deleted_at IS NOT NULL"); + query.push_str(" WHERE model_categories.deleted_at IS NULL"); first_predicate_added = true; } else { - query.push_str(" AND model_categories.deleted_at IS NOT NULL"); + query.push_str(" AND model_categories.deleted_at IS NULL"); } } @@ -284,7 +283,7 @@ mod tests { assert_eq!(&query_builder.build_predicates(), " WHERE model_categories.is_mod_approved IS TRUE \ - AND model_categories.deleted_at IS NOT NULL"); + AND model_categories.deleted_at IS NULL"); } #[test] @@ -295,7 +294,7 @@ mod tests { assert_eq!(&query_builder.build_predicates(), " WHERE model_categories.creator_user_token = ? \ AND model_categories.is_mod_approved IS TRUE \ - AND model_categories.deleted_at IS NOT NULL"); + AND model_categories.deleted_at IS NULL"); } #[test] @@ -306,7 +305,7 @@ mod tests { assert_eq!(&query_builder.build_predicates(), " WHERE model_categories.model_type = ? \ AND model_categories.is_mod_approved IS TRUE \ - AND model_categories.deleted_at IS NOT NULL"); + AND model_categories.deleted_at IS NULL"); } #[test] @@ -316,13 +315,13 @@ mod tests { assert_eq!(&query_builder.build_predicates(), " WHERE model_categories.is_mod_approved IS TRUE \ - AND model_categories.deleted_at IS NOT NULL"); + AND model_categories.deleted_at IS NULL"); let query_builder = ListCategoriesQueryBuilder::new() .show_unapproved(true); assert_eq!(&query_builder.build_predicates(), - " WHERE model_categories.deleted_at IS NOT NULL"); + " WHERE model_categories.deleted_at IS NULL"); } #[test] @@ -332,7 +331,7 @@ mod tests { assert_eq!(&query_builder.build_predicates(), " WHERE model_categories.is_mod_approved IS TRUE \ - AND model_categories.deleted_at IS NOT NULL"); + AND model_categories.deleted_at IS NULL"); let query_builder = ListCategoriesQueryBuilder::new() .show_deleted(true); diff --git a/crates/app/storyteller_web/src/http_server/endpoints/categories/list_categories.rs b/crates/app/storyteller_web/src/http_server/endpoints/categories/list_categories.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/app/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs b/crates/app/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs new file mode 100644 index 0000000000..832fc12d9e --- /dev/null +++ b/crates/app/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs @@ -0,0 +1,143 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_web::HttpResponseBuilder; +use actix_web::cookie::Cookie; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::{Path, Json}; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use crate::database::enums::record_visibility::RecordVisibility; +use crate::database::queries::query_w2l_template::select_w2l_template_by_token; +use crate::database::query_builders::list_categories_query_builder::ListCategoriesQueryBuilder; +use crate::http_server::web_utils::ip_address::get_request_ip; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::server_state::ServerState; +use crate::util::email_to_gravatar::email_to_gravatar; +use crate::util::markdown_to_html::markdown_to_html; +use crate::util::random_crockford_token::random_crockford_token; +use crate::util::random_prefix_crockford_token::random_prefix_crockford_token; +use crate::validations::check_for_slurs::contains_slurs; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use log::{info, warn, log}; +use regex::Regex; +use sqlx::MySqlPool; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::fmt; +use std::sync::Arc; + +// =============== Success Response =============== + +#[derive(Serialize)] +pub struct ListTtsCategoriesResponse { + pub success: bool, + pub categories: Vec, +} + +/// Public-facing and optimized (non-irrelevant fields) category list +/// Used for the main TTS dropdown as well as the TTS edit/tagging UI +#[derive(Serialize)] +pub struct DisplayCategory { + pub category_token: String, + + pub model_type: String, // TODO: ENUM + + pub maybe_super_category_token: Option, + + pub can_directly_have_models: bool, + pub can_have_subcategories: bool, + pub can_only_mods_apply: bool, + + pub name: String, + pub maybe_dropdown_name: Option, + + //pub creator_user_token: Option, + //pub creator_username: Option, + //pub creator_display_name: Option, +} + +// =============== Error Response =============== + +#[derive(Debug, Serialize)] +pub enum ListTtsCategoriesError { + NotAuthorized, + ServerError, +} + +impl ResponseError for ListTtsCategoriesError { + fn status_code(&self) -> StatusCode { + match *self { + ListTtsCategoriesError::NotAuthorized => StatusCode::UNAUTHORIZED, + ListTtsCategoriesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using DeriveMore since Clion doesn't understand it. +impl fmt::Display for ListTtsCategoriesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + +pub async fn list_tts_categories_handler( + http_request: HttpRequest, + server_state: web::Data>) -> Result +{ + let query_builder = ListCategoriesQueryBuilder::new() + .show_deleted(false) + .show_unapproved(false) + .scope_model_type(Some("tts")); + + let query_result = + query_builder.perform_query(&server_state.mysql_pool).await; + + let results = match query_result { + Ok(results) => results, + Err(err) => { + warn!("DB error: {:?}", err); + return Err(ListTtsCategoriesError::ServerError); + } + }; + + warn!("Number of categories: {:?}", results.categories.len()); + + let mut categories = results.categories.iter() + .map(|c| { + DisplayCategory { + category_token: c.category_token.clone(), + model_type: c.model_type.clone(), + maybe_super_category_token: c.maybe_super_category_token.clone(), + can_directly_have_models: c.can_directly_have_models, + can_have_subcategories: c.can_have_subcategories, + can_only_mods_apply: c.can_only_mods_apply, + name: c.name.clone(), + maybe_dropdown_name:c.maybe_dropdown_name.clone(), + } + }) + .collect::>(); + + // TODO: Sort by dropdown name too. + categories.sort_by(|c1, c2| c1.name.cmp(&c2.name)); + + // TODO: Cache results in Redis w/ TTL. + let response = ListTtsCategoriesResponse { + success: true, + categories, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| ListTtsCategoriesError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/app/storyteller_web/src/http_server/endpoints/categories/mod.rs b/crates/app/storyteller_web/src/http_server/endpoints/categories/mod.rs index 621958b942..48b65823da 100644 --- a/crates/app/storyteller_web/src/http_server/endpoints/categories/mod.rs +++ b/crates/app/storyteller_web/src/http_server/endpoints/categories/mod.rs @@ -1,5 +1,5 @@ pub mod create_category; pub mod delete_category; pub mod edit_category; -pub mod list_categories; +pub mod list_tts_categories; pub mod list_my_categories; \ No newline at end of file diff --git a/crates/app/storyteller_web/src/routes.rs b/crates/app/storyteller_web/src/routes.rs index 742690606e..4a434604e5 100644 --- a/crates/app/storyteller_web/src/routes.rs +++ b/crates/app/storyteller_web/src/routes.rs @@ -62,6 +62,7 @@ use crate::http_server::endpoints::w2l::list_w2l_templates::list_w2l_templates_h use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler; use crate::http_server::endpoints::categories::delete_category::delete_category_handler; use crate::http_server::endpoints::categories::edit_category::edit_category_handler; +use crate::http_server::endpoints::categories::list_tts_categories::list_tts_categories_handler; pub fn add_routes (app: App) -> App where @@ -426,6 +427,14 @@ fn add_category_routes (app: App) -> App .route(web::post().to(edit_category_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service( + web::scope("/list") + .service( + web::resource("/tts") + .route(web::get().to(list_tts_categories_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) ) }