diff --git a/Cargo.lock b/Cargo.lock index 9326263b..72ae1baf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,16 +348,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "chrono-tz" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" -dependencies = [ - "chrono", - "phf 0.12.1", -] - [[package]] name = "clap" version = "4.6.1" @@ -1039,7 +1029,7 @@ dependencies = [ "indexmap", "lasso", "once_cell", - "phf 0.11.3", + "phf", "rand", ] @@ -1797,7 +1787,6 @@ dependencies = [ "async-trait", "axum", "chrono", - "chrono-tz", "colored", "config", "figlet-rs", @@ -2037,16 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_shared 0.12.1", + "phf_shared", ] [[package]] @@ -2055,7 +2035,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared 0.11.3", + "phf_shared", "rand", ] @@ -2066,7 +2046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", - "phf_shared 0.11.3", + "phf_shared", "proc-macro2", "quote", "syn", @@ -2081,15 +2061,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project-lite" version = "0.2.17" diff --git a/Cargo.toml b/Cargo.toml index 25dacfb3..21e081ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ axum = { version = "0.8" } base64ct = { version = "1.8", features = ["alloc"] } change-detection = "1.2" chrono = "0.4" -chrono-tz = "0.10" colored = "3.1" config = { version = "0.15", default-features = false, features = ["toml"] } figlet-rs = "1.0" @@ -112,7 +111,6 @@ testing = [] async-trait.workspace = true axum.workspace = true chrono.workspace = true -chrono-tz.workspace = true colored.workspace = true config.workspace = true figlet-rs.workspace = true diff --git a/examples/intro-datetime.rs b/examples/intro-datetime.rs deleted file mode 100644 index c15afb0b..00000000 --- a/examples/intro-datetime.rs +++ /dev/null @@ -1,270 +0,0 @@ -use pagetop::prelude::*; - -include_locales!(LOC from "examples/locale"); - -struct IntroDatetime; - -#[async_trait] -impl Extension for IntroDatetime { - fn configure_router(&self, router: Router) -> Router { - router.route("/", web::get(intro_datetime)) - } -} - -async fn intro_datetime(request: HttpRequest) -> Result { - Page::new(request) - .with_child( - Intro::custom() - .with_title(Lc::n("PageTop")) - .with_slogan(Lc::t("datetime_slogan", &LOC)) - .with_child(world_block()) - .with_child(formats_block()) - .with_child(relative_block()) - .with_child(since_until_block()), - ) - .render() - .await -} - -// Zonas IANA de la demostración, con la clave de su etiqueta traducible. -const ZONES: [(&str, &str); 4] = [ - ("datetime_zone_utc", "UTC"), - ("datetime_zone_madrid", "Europe/Madrid"), - ("datetime_zone_mexico", "America/Mexico_City"), - ("datetime_zone_tokyo", "Asia/Tokyo"), -]; - -// El mismo instante (`Utc::now()`) mostrado en cada zona de `ZONES`: fecha y hora combinadas -// (`DateFormat::Medium` + `TimeFormat::Short`) y su equivalente ISO 8601, con el offset propio de -// cada zona. -fn world_block() -> Block { - Block::new() - .with_title(Lc::t("datetime_block_world", &LOC)) - .with_child(Html::with(|cx| { - let now = Utc::now(); - let rows: Vec<(Lc, String, String)> = ZONES - .into_iter() - .map(|(label_key, zone)| { - let tz: Tz = zone.parse().expect("valid IANA timezone"); - let zone_cx = Context::default().with_langid(cx).with_timezone(tz); - ( - Lc::t(label_key, &LOC), - zone_cx.format_datetime(now, DateFormat::Medium, TimeFormat::Short), - zone_cx.format_iso_datetime(now), - ) - }) - .collect(); - - html! { - ul { - @for (label, combined, iso) in &rows { - li { - strong { (label.using(cx)) ": " } (combined) - " (ISO " code { (iso) } ")" - } - } - } - } - })) -} - -// Combinaciones de `DateFormat`/`TimeFormat` sobre la zona horaria efectiva del visitante. Para -// añadir un nuevo formato basta con ampliar el array correspondiente. -fn formats_block() -> Block { - Block::new() - .with_title(Lc::t("datetime_block_formats", &LOC)) - .with_child(Html::with(|cx| { - let now = Utc::now(); - let today = now.with_timezone(&cx.timezone()).date_naive(); - - let dates: Vec<(&str, String)> = [ - ("Short", DateFormat::Short), - ("Medium", DateFormat::Medium), - ("Long", DateFormat::Long), - ("Iso", DateFormat::Iso), - ("Custom(\"%d.%m.%Y\")", DateFormat::Custom("%d.%m.%Y")), - ] - .into_iter() - .map(|(label, format)| (label, cx.format_date(today, format))) - .collect(); - - let times: Vec<(&str, String)> = [ - ("Short", TimeFormat::Short), - ("Long", TimeFormat::Long), - ("Custom(\"%I:%M %p\")", TimeFormat::Custom("%I:%M %p")), - ] - .into_iter() - .map(|(label, format)| (label, cx.format_time(now, format))) - .collect(); - - let combos: Vec<(&str, String)> = [ - ("Short + Short", DateFormat::Short, TimeFormat::Short), - ("Medium + Short", DateFormat::Medium, TimeFormat::Short), - ("Long + Long", DateFormat::Long, TimeFormat::Long), - ("Long + Short", DateFormat::Long, TimeFormat::Short), - ("Iso + Long", DateFormat::Iso, TimeFormat::Long), - ] - .into_iter() - .map(|(label, date, time)| (label, cx.format_datetime(now, date, time))) - .collect(); - - html! { - h3 { (Lc::t("datetime_formats_date", &LOC).using(cx)) } - ul { - @for (label, value) in &dates { - li { code { (label) } ": " (value) } - } - } - h3 { (Lc::t("datetime_formats_time", &LOC).using(cx)) } - ul { - @for (label, value) in × { - li { code { (label) } ": " (value) } - } - } - h3 { (Lc::t("datetime_formats_combos", &LOC).using(cx)) } - ul { - @for (label, value) in &combos { - li { code { (label) } ": " (value) } - } - } - p { - (Lc::t("datetime_formats_iso", &LOC).using(cx)) - " " code { (cx.format_iso_datetime(now)) } - } - } - })) -} - -// Casos ilustrativos de `RelativeFormat` sobre la zona horaria efectiva del visitante: hoy, un -// desplazamiento simple de días, uno con el componente de meses en cero, uno con los tres -// componentes y uno en el futuro -- siempre respecto al día actual. Añadir un caso nuevo es sólo -// ampliar `samples`. La primera columna muestra la fecha seleccionada (no una etiqueta fija), -// formateada con `DateFormat::Medium` en el idioma efectivo, para que se vea a qué fecha concreta -// corresponde cada resultado relativo. -fn relative_block() -> Block { - Block::new() - .with_title(Lc::t("datetime_block_relative", &LOC)) - .with_child(Html::with(|cx| { - let today = Utc::now().date_naive(); - - // Desplazamientos de meses/años construidos con `checked_sub_months()` (mismo mecanismo - // que usa `RelativeFormat` por dentro), para que el resultado coincida exactamente con - // el criterio de cada caso. - let two_years_and_3_days_ago = today - .checked_sub_months(Months::new(24)) - .expect("valid date") - - Duration::days(3); - let three_years_2_months_10_days_ago = today - .checked_sub_months(Months::new(38)) - .expect("valid date") - - Duration::days(10); - - let samples = [ - today, - today - Duration::days(3), - two_years_and_3_days_ago, - three_years_2_months_10_days_ago, - today + Duration::days(5), - ]; - - let rows: Vec<(String, String, String, String)> = samples - .into_iter() - .map(|date| { - let dt = at_noon(date); - ( - cx.format_date(date, DateFormat::Medium), - cx.format_relative(dt, RelativeFormat::Short), - cx.format_relative(dt, RelativeFormat::Medium), - cx.format_relative(dt, RelativeFormat::Long), - ) - }) - .collect(); - - html! { - table style="width: 100%; border: 1px solid black;" { - thead { - tr { - th { (Lc::t("datetime_relative_col_date", &LOC).using(cx)) } - th { "Short" } - th { "Medium" } - th { "Long" } - } - } - tbody { - @for (date, short, medium, long) in &rows { - tr { - td { (date) } - td { (short) } - td { (medium) } - td { (long) } - } - } - } - } - } - })) -} - -// Niveles de `DatePrecision` para `format_since()`/`format_until()`. Añadir un nivel nuevo es sólo -// ampliar este array. -const PRECISIONS: [(&str, DatePrecision); 3] = [ - ("Short", DatePrecision::Short), - ("Medium", DatePrecision::Medium), - ("Long", DatePrecision::Long), -]; - -// `format_since()`/`format_until()` sobre una única fecha de ejemplo: a diferencia de `DateFormat`, -// lo que cambia entre niveles no es el estilo, sino la propia precisión revelada (sólo el mes, mes -// y año, o fecha completa). -fn since_until_block() -> Block { - Block::new() - .with_title(Lc::t("datetime_block_since_until", &LOC)) - .with_child(Html::with(|cx| { - let date = NaiveDate::from_ymd_opt(2026, 6, 3).unwrap(); - - let rows: Vec<(&str, String, String)> = PRECISIONS - .into_iter() - .map(|(label, precision)| { - ( - label, - cx.format_since(date, precision), - cx.format_until(date, precision), - ) - }) - .collect(); - - html! { - table style="width: 100%; border: 1px solid black;" { - thead { - tr { - th { (Lc::t("datetime_since_until_col_precision", &LOC).using(cx)) } - th { (Lc::t("datetime_since_until_col_since", &LOC).using(cx)) } - th { (Lc::t("datetime_since_until_col_until", &LOC).using(cx)) } - } - } - tbody { - @for (label, since, until) in &rows { - tr { - td { (*label) } - td { (since) } - td { (until) } - } - } - } - } - } - })) -} - -// Mediodía, para evitar que el redondeo horario de la conversión de zona horaria empuje la fecha -// civil resultante al día anterior o siguiente en zonas con un offset amplio. -fn at_noon(date: NaiveDate) -> DateTime { - date.and_hms_opt(12, 0, 0) - .expect("noon is always a valid time") - .and_utc() -} - -#[pagetop::main] -async fn main() -> std::io::Result<()> { - Application::prepare(&IntroDatetime).await.run().await -} diff --git a/examples/locale/en-US/intro-datetime.ftl b/examples/locale/en-US/intro-datetime.ftl deleted file mode 100644 index 5b0fd436..00000000 --- a/examples/locale/en-US/intro-datetime.ftl +++ /dev/null @@ -1,21 +0,0 @@ -datetime_slogan = Timezones and date/time formats - -datetime_block_world = The same instant, around the world -datetime_zone_utc = UTC -datetime_zone_madrid = Madrid (Europe/Madrid) -datetime_zone_mexico = Mexico City (America/Mexico_City) -datetime_zone_tokyo = Tokyo (Asia/Tokyo) - -datetime_block_formats = DateFormat / TimeFormat combinations -datetime_formats_date = DateFormat (date only) -datetime_formats_time = TimeFormat (time only) -datetime_formats_combos = format_datetime() combinations (date + time) -datetime_formats_iso = format_iso_datetime(): - -datetime_block_relative = RelativeFormat -datetime_relative_col_date = Date - -datetime_block_since_until = DatePrecision (format_since / format_until) -datetime_since_until_col_precision = Precision -datetime_since_until_col_since = since -datetime_since_until_col_until = until diff --git a/examples/locale/es-ES/intro-datetime.ftl b/examples/locale/es-ES/intro-datetime.ftl deleted file mode 100644 index 6800682e..00000000 --- a/examples/locale/es-ES/intro-datetime.ftl +++ /dev/null @@ -1,21 +0,0 @@ -datetime_slogan = Zonas horarias y formatos de fecha y hora - -datetime_block_world = El mismo instante, alrededor del mundo -datetime_zone_utc = UTC -datetime_zone_madrid = Madrid (Europe/Madrid) -datetime_zone_mexico = Ciudad de México (America/Mexico_City) -datetime_zone_tokyo = Tokio (Asia/Tokyo) - -datetime_block_formats = Combinaciones de DateFormat / TimeFormat -datetime_formats_date = DateFormat (sólo fecha) -datetime_formats_time = TimeFormat (sólo hora) -datetime_formats_combos = Combinaciones de format_datetime() (fecha + hora) -datetime_formats_iso = format_iso_datetime(): - -datetime_block_relative = RelativeFormat -datetime_relative_col_date = Fecha - -datetime_block_since_until = DatePrecision (format_since / format_until) -datetime_since_until_col_precision = Precisión -datetime_since_until_col_since = desde -datetime_since_until_col_until = hasta diff --git a/extensions/pagetop-admin/src/entity/setting.rs b/extensions/pagetop-admin/src/entity/setting.rs index 8907f0fd..95b6c4c6 100644 --- a/extensions/pagetop-admin/src/entity/setting.rs +++ b/extensions/pagetop-admin/src/entity/setting.rs @@ -1,3 +1,4 @@ +use pagetop::prelude::*; use pagetop_seaorm::db::*; /// Entidad SeaORM para la tabla `settings`. @@ -8,7 +9,7 @@ pub struct Model { pub key: String, pub scope: String, pub value: String, - pub updated_at: DateTimeUtc, + pub updated_at: NaiveDateTime, pub updated_by: Option, } diff --git a/extensions/pagetop-admin/src/registry.rs b/extensions/pagetop-admin/src/registry.rs index 4ebbbd4b..3adcfd50 100644 --- a/extensions/pagetop-admin/src/registry.rs +++ b/extensions/pagetop-admin/src/registry.rs @@ -14,7 +14,7 @@ use crate::action::{ }; use crate::settings::SettingsSchema; -// **< AdminPermission >**************************************************************************** +// **< AdminPermission >***************************************************************************** /// Permisos propios de `pagetop-admin`. #[derive(Clone, Copy, Debug)] @@ -43,7 +43,7 @@ impl Permission for AdminPermission { } } -// **< Tipos del registro >************************************************************************* +// **< Tipos del registro >************************************************************************** /// Sección del panel de administración (agrupación en el sidebar). #[derive(Clone)] @@ -143,7 +143,7 @@ pub struct AdminAction { pub weight: i32, } -// **< AdminRegistry >****************************************************************************** +// **< AdminRegistry >******************************************************************************* /// Registro global del panel de administración, construido una sola vez en `initialize()`. #[derive(Getters)] diff --git a/extensions/pagetop-admin/src/settings.rs b/extensions/pagetop-admin/src/settings.rs index d145709d..60f73637 100644 --- a/extensions/pagetop-admin/src/settings.rs +++ b/extensions/pagetop-admin/src/settings.rs @@ -11,7 +11,7 @@ use serde::{Serialize, de::DeserializeOwned}; use crate::entity::setting::{ActiveModel, Column, Entity}; use crate::error::AdminError; -// **< API pública >******************************************************************************** +// **< API pública >********************************************************************************* /// Lee un valor persistido, devolviendo el `Default` del tipo si no existe. pub async fn get(key: &str) -> T { @@ -40,7 +40,7 @@ pub async fn list_scope(scope: &str) -> Vec<(String, String)> { list_scope_async(scope).await.unwrap_or_default() } -// **< Implementación asíncrona >******************************************************************* +// **< Implementación asíncrona >******************************************************************** async fn get_async(key: &str) -> Result { let model = Entity::find_by_id(key) @@ -57,7 +57,7 @@ async fn set_async( user_id: Option, ) -> Result<(), AdminError> { let value_json = serde_json::to_string(value)?; - let now = Utc::now(); + let now = Utc::now().naive_utc(); let existing = Entity::find_by_id(key).one(dbconn()).await?; if existing.is_some() { @@ -95,7 +95,7 @@ async fn list_scope_async(scope: &str) -> Result, AdminErr Ok(rows.into_iter().map(|m| (m.key, m.value)).collect()) } -// **< Tipos de esquema >*************************************************************************** +// **< Tipos de esquema >**************************************************************************** /// Tipo de campo de configuración para un [`SettingsSchema`]. #[derive(Clone, Debug)] diff --git a/extensions/pagetop-htmx/tests/extension.rs b/extensions/pagetop-htmx/tests/extension.rs index 36c21704..c0452ada 100644 --- a/extensions/pagetop-htmx/tests/extension.rs +++ b/extensions/pagetop-htmx/tests/extension.rs @@ -42,7 +42,7 @@ async fn htmx_script_is_served_at_the_expected_static_path() { assert!(body.contains("htmx")); } -// **< Automatic script injection (BeforeRenderBody) >********************************************** +// **< Automatic script injection (BeforeRenderBody) >*********************************************** #[pagetop::test] async fn rendered_pages_automatically_include_the_pinned_htmx_script_tag() { diff --git a/extensions/pagetop-seaorm/src/migration.rs b/extensions/pagetop-seaorm/src/migration.rs index 412426aa..0c974f06 100644 --- a/extensions/pagetop-seaorm/src/migration.rs +++ b/extensions/pagetop-seaorm/src/migration.rs @@ -103,7 +103,7 @@ //! | `string_uniq(col)` | `VARCHAR NOT NULL UNIQUE` | //! | `integer(col)` | `INTEGER NOT NULL` | //! | `boolean(col)` | `BOOLEAN NOT NULL` | -//! | `timestamp(col)` | `TIMESTAMP WITH TIME ZONE NOT NULL` | +//! | `timestamp(col)` | `TIMESTAMP NOT NULL` | //! | `uuid(col)` | `UUID NOT NULL` | //! //! Estas son sólo las funciones más habituales. El módulo [`schema`] define la lista completa, con diff --git a/extensions/pagetop-seaorm/src/migration/schema.rs b/extensions/pagetop-seaorm/src/migration/schema.rs index 1d645c5d..88ce8b13 100644 --- a/extensions/pagetop-seaorm/src/migration/schema.rs +++ b/extensions/pagetop-seaorm/src/migration/schema.rs @@ -329,16 +329,12 @@ pub fn interval_uniq( interval(col, fields, precision).unique_key().take() } -// Unlike the original helpers, `timestamp()` and `timestamp_null()` create time zone aware -// columns. Entities store instants as `DateTimeUtc`, which PostgreSQL can only decode from -// `TIMESTAMPTZ`; a plain `TIMESTAMP` would also store `CURRENT_TIMESTAMP` in server local time. -// Keep this deviation when syncing with upstream. pub fn timestamp(col: T) -> ColumnDef { - timestamp_with_time_zone(col) + ColumnDef::new(col).timestamp().not_null().take() } pub fn timestamp_null(col: T) -> ColumnDef { - timestamp_with_time_zone_null(col) + ColumnDef::new(col).timestamp().null().take() } pub fn timestamp_uniq(col: T) -> ColumnDef { diff --git a/extensions/pagetop-user/src/auth.rs b/extensions/pagetop-user/src/auth.rs index 6d737bda..67d21aa6 100644 --- a/extensions/pagetop-user/src/auth.rs +++ b/extensions/pagetop-user/src/auth.rs @@ -24,7 +24,7 @@ pub async fn login( plain_password: &str, remember: bool, ) -> Result { - let now = Utc::now(); + let now = Utc::now().naive_utc(); // Buscar usuario por username o email. let user_model = user::Entity::find() @@ -119,7 +119,7 @@ pub async fn register( } let hash = password::hash_password(plain_password)?; - let now = Utc::now(); + let now = Utc::now().naive_utc(); let status = if SETTINGS.require_email_verification { UserStatus::Pending } else { @@ -175,7 +175,7 @@ pub async fn assign_role(user_id: i32, role_id: i32) -> Result<(), AuthError> { async fn register_failed_login( user_model: &user::Model, - now: DateTime, + now: NaiveDateTime, ) -> Result<(), AuthError> { let new_count = user_model.failed_login_count + 1; let lock_at = if new_count >= SETTINGS.max_failed_logins { @@ -236,7 +236,7 @@ async fn do_seed() { } }; - let now = Utc::now(); + let now = Utc::now().naive_utc(); let new_admin = user::ActiveModel { id: ActiveValue::NotSet, username: Set(cfg.admin_username.clone()), diff --git a/extensions/pagetop-user/src/component/admin.rs b/extensions/pagetop-user/src/component/admin.rs index 01835906..296b3370 100644 --- a/extensions/pagetop-user/src/component/admin.rs +++ b/extensions/pagetop-user/src/component/admin.rs @@ -22,16 +22,15 @@ use pagetop::prelude::*; use crate::LOCALES_USER; -// **< constantes compartidas >********************************************************************* +// **< constantes compartidas >********************************************************************** /// Identificador del `
` de [`UserForm`] en modo [`UserFormMode::Edit`]. En ese modo, el botón -/// "Guardar" no se renderiza dentro del formulario (ver `UserForm::prepare()`). Lo añade la -/// pantalla de edición (`handlers::admin::users::edit_actions()`) junto al resto de acciones, -/// referenciando este `id` mediante el atributo `form` para seguir enviando el formulario aunque -/// esté fuera de él. +/// "Guardar" no se renderiza dentro del formulario (ver `UserForm::prepare()`): lo añade la pantalla +/// de edición (`handlers::admin::users::edit_actions()`) junto al resto de acciones, referenciando +/// este id mediante el atributo `form` para seguir enviando el formulario aunque esté fuera de él. pub(crate) const USER_ADMIN_FORM_ID: &str = "user-admin-form"; -// **< tipos compartidos >************************************************************************** +// **< tipos compartidos >*************************************************************************** /// Un permiso dentro de un grupo del catálogo: `(clave, etiqueta, concedido)`. pub(crate) type PermissionItem = (CowStr, Lc, bool); @@ -39,12 +38,12 @@ pub(crate) type PermissionItem = (CowStr, Lc, bool); /// Catálogo de permisos agrupado: `(título del grupo, permisos del grupo)`. pub(crate) type PermissionGroups = Vec<(Lc, Vec)>; -// **< HELPERS >************************************************************************************ +// **< helpers compartidos >************************************************************************* -// `Fieldset` con las casillas para asignar roles (usado en el alta de usuario y en la pantalla de -// asignación de roles). El rol "authenticated" no se lista como casilla ni se envía: todo usuario -// autenticado lo tiene concedido por definición (ver `session::load_user_from_session`), sin -// necesidad de una fila en `user_role`. +// `Fieldset` con las casillas para asignar roles (usado en el alta de usuario y en la pantalla +// dedicada de asignación de roles). El rol "authenticated" no se lista como casilla ni se envía: +// todo usuario autenticado lo tiene concedido por definición (ver `session::load_user_from_session`), +// sin necesidad de una fila en `user_role`. pub(crate) fn roles_fieldset(roles: &[(i32, String, bool)]) -> form::Fieldset { let mut field = form::check::Field::new().with_name("role_ids"); for (role_id, label, checked) in roles { diff --git a/extensions/pagetop-user/src/component/admin/user_form.rs b/extensions/pagetop-user/src/component/admin/user_form.rs index 42ccd0e2..621f5d9e 100644 --- a/extensions/pagetop-user/src/component/admin/user_form.rs +++ b/extensions/pagetop-user/src/component/admin/user_form.rs @@ -1,8 +1,5 @@ //! Formulario de alta/edición de usuario. -use std::collections::BTreeMap; -use std::sync::LazyLock; - use pagetop::prelude::*; use crate::ADMIN_USERS_PATH; @@ -13,32 +10,6 @@ use crate::component::{PasswordConfirm, error_banner}; use super::{USER_ADMIN_FORM_ID, roles_fieldset}; -// Regiones de la base IANA que sólo contienen alias heredados (fichero `backward`), todos con una -// zona canónica equivalente en otra región (p. ej. `US/Eastern` es `America/New_York`). -const LEGACY_REGIONS: [&str; 5] = ["Brazil", "Canada", "Chile", "Mexico", "US"]; - -// Zonas horarias IANA canónicas agrupadas por región (lo anterior a la primera `/`), ordenadas por -// región y nombre. Se descartan los alias heredados: los nombres sin región (`GB`, `Japan`, -// `EST5EDT`...), los de `LEGACY_REGIONS` y los de `Etc` salvo `Etc/UTC`, cuyo grupo va al final. -static TZ_BY_REGION: LazyLock)>> = LazyLock::new(|| { - let mut regions: BTreeMap<&'static str, Vec<&'static str>> = BTreeMap::new(); - for tz in TZ_VARIANTS.iter() { - let name = tz.name(); - let Some((region, _)) = name.split_once('/') else { - continue; - }; - if LEGACY_REGIONS.contains(®ion) || (region == "Etc" && name != "Etc/UTC") { - continue; - } - regions.entry(region).or_default().push(name); - } - for names in regions.values_mut() { - names.sort_unstable(); - } - let etc = regions.remove_entry("Etc"); - regions.into_iter().chain(etc).collect() -}); - #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub(crate) enum UserFormMode { #[default] @@ -110,13 +81,17 @@ impl Component for UserForm { .with_value(self.language()) .with_label(Lc::t("field-language", &LOCALES_USER)), ) - .with_child(timezone_field(self.timezone())); + .with_child( + form::input::Field::text() + .with_name("timezone") + .with_value(self.timezone()) + .with_label(Lc::t("field-timezone", &LOCALES_USER)), + ); if *self.mode() == UserFormMode::New { - form = form.with_child(PasswordConfirm::new()); - if !self.roles().is_empty() { - form = form.with_child(roles_fieldset(self.roles())); - } + form = form + .with_child(PasswordConfirm::new()) + .with_child(roles_fieldset(self.roles())); if *self.allow_admin_field() { form = form.with_child( @@ -206,29 +181,3 @@ impl UserForm { self } } - -// `