diff --git a/Cargo.lock b/Cargo.lock index ae768040..9b0b1771 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -817,6 +817,7 @@ dependencies = [ name = "example-admin" version = "0.1.0" dependencies = [ + "async-trait", "cot", "rinja", ] diff --git a/cot/src/admin.rs b/cot/src/admin.rs index 4aa0ae87..34ace5cc 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -207,7 +207,7 @@ async fn view_model( let total_object_counts = manager.get_total_object_counts(&request).await?; let total_pages = total_object_counts.div_ceil(page_size); - if page == 0 || page > total_pages { + if (page == 0 || page > total_pages) && total_pages > 0 { return Err(Error::not_found_message(format!("page {page} not found"))); } diff --git a/examples/admin/Cargo.toml b/examples/admin/Cargo.toml index e3d95fa0..f4e507bc 100644 --- a/examples/admin/Cargo.toml +++ b/examples/admin/Cargo.toml @@ -3,8 +3,9 @@ name = "example-admin" version = "0.1.0" publish = false description = "Admin panel - Cot example." -edition = "2021" +edition = "2024" [dependencies] +async-trait = "0.1" cot = { path = "../../cot", features = ["live-reload"] } rinja = "0.3.5" diff --git a/examples/admin/src/main.rs b/examples/admin/src/main.rs index ea963642..7708dc1f 100644 --- a/examples/admin/src/main.rs +++ b/examples/admin/src/main.rs @@ -1,24 +1,53 @@ -use cot::__private::async_trait; -use cot::admin::AdminApp; +mod migrations; + +use std::fmt::{Display, Formatter}; + +use async_trait::async_trait; +use cot::admin::{AdminApp, AdminModel, AdminModelManager, DefaultAdminModelManager}; use cot::auth::db::{DatabaseUser, DatabaseUserApp}; use cot::cli::CliMetadata; -use cot::config::{DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig}; +use cot::config::{ + AuthBackendConfig, DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig, +}; +use cot::db::migrations::SyncDynMigration; +use cot::db::{Auto, Model, model}; +use cot::form::Form; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext}; +use cot::request::extractors::RequestDb; use cot::response::{Response, ResponseExt}; use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Body, BoxedHandler, Project, ProjectContext, StatusCode}; use rinja::Template; +#[derive(Debug, Clone, Form, AdminModel)] +#[model] +struct TodoItem { + #[model(primary_key)] + id: Auto, + title: String, +} + +impl Display for TodoItem { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.title) + } +} + #[derive(Debug, Template)] #[template(path = "index.html")] struct IndexTemplate<'a> { urls: &'a Urls, + todo_items: Vec, } -async fn index(urls: Urls) -> cot::Result { - let index_template = IndexTemplate { urls: &urls }; +async fn index(urls: Urls, RequestDb(db): RequestDb) -> cot::Result { + let todo_items = TodoItem::objects().all(&db).await?; + let index_template = IndexTemplate { + urls: &urls, + todo_items, + }; let rendered = index_template.render()?; Ok(Response::new_html(StatusCode::OK, Body::fixed(rendered))) @@ -42,6 +71,14 @@ impl App for HelloApp { Ok(()) } + fn migrations(&self) -> Vec> { + cot::db::migrations::wrap_migrations(migrations::MIGRATIONS) + } + + fn admin_model_managers(&self) -> Vec> { + vec![Box::new(DefaultAdminModelManager::::new())] + } + fn router(&self) -> Router { Router::with_urls([Route::with_handler("/", index)]) } @@ -62,6 +99,7 @@ impl Project for AdminProject { .url("sqlite://db.sqlite3?mode=rwc") .build(), ) + .auth_backend(AuthBackendConfig::Database) .middlewares( MiddlewareConfig::builder() .session(SessionMiddlewareConfig::builder().secure(false).build()) diff --git a/examples/admin/src/migrations.rs b/examples/admin/src/migrations.rs new file mode 100644 index 00000000..c1cd016a --- /dev/null +++ b/examples/admin/src/migrations.rs @@ -0,0 +1,7 @@ +//! List of migrations for the current app. +//! +//! Generated by cot CLI 0.2.0 on 2025-03-28 19:43:54+00:00 + +pub mod m_0001_initial; +/// The list of migrations for current app. +pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[&m_0001_initial::Migration]; diff --git a/examples/admin/src/migrations/m_0001_initial.rs b/examples/admin/src/migrations/m_0001_initial.rs new file mode 100644 index 00000000..d78ea165 --- /dev/null +++ b/examples/admin/src/migrations/m_0001_initial.rs @@ -0,0 +1,35 @@ +//! Generated by cot CLI 0.2.0 on 2025-03-28 19:43:54+00:00 + +#[derive(Debug, Copy, Clone)] +pub(super) struct Migration; +impl ::cot::db::migrations::Migration for Migration { + const APP_NAME: &'static str = "example-admin"; + const MIGRATION_NAME: &'static str = "m_0001_initial"; + const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = &[]; + const OPERATIONS: &'static [::cot::db::migrations::Operation] = + &[::cot::db::migrations::Operation::create_model() + .table_name(::cot::db::Identifier::new("todo_item")) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("title"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) + .build()]; +} + +#[derive(::core::fmt::Debug)] +#[::cot::db::model(model_type = "migration")] +struct _TodoItem { + #[model(primary_key)] + id: cot::db::Auto, + title: String, +} diff --git a/examples/admin/templates/index.html b/examples/admin/templates/index.html index 6c6ca108..176e2d9d 100644 --- a/examples/admin/templates/index.html +++ b/examples/admin/templates/index.html @@ -8,8 +8,21 @@ Admin Panel example -

Hello!

-

Go to the admin panel.

+

Admin panel-controlled TODO list

+ +{% if todo_items.is_empty() %} +

There are no TODO items.

+{% else %} +
    + {% for todo in todo_items %} +
  • + {{ todo.title }} +
  • + {% endfor %} +
+{% endif %} + +

Go to the admin panel to manage the TODO items.

The username is admin and the password is admin.

diff --git a/examples/custom-error-pages/Cargo.toml b/examples/custom-error-pages/Cargo.toml index a305b3d5..0e11a1bb 100644 --- a/examples/custom-error-pages/Cargo.toml +++ b/examples/custom-error-pages/Cargo.toml @@ -3,7 +3,7 @@ name = "example-custom-error-pages" version = "0.1.0" publish = false description = "Custom Error Pages - Cot example." -edition = "2021" +edition = "2024" [dependencies] cot = { path = "../../cot" } diff --git a/examples/custom-task/Cargo.toml b/examples/custom-task/Cargo.toml index 766f9c3b..6ce1e2fe 100644 --- a/examples/custom-task/Cargo.toml +++ b/examples/custom-task/Cargo.toml @@ -3,7 +3,7 @@ name = "example-custom-task" version = "0.1.0" publish = false description = "Custom tasks - Cot example." -edition = "2021" +edition = "2024" [dependencies] async-trait = "0.1" diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml index 691506e9..423b5a73 100644 --- a/examples/hello-world/Cargo.toml +++ b/examples/hello-world/Cargo.toml @@ -3,7 +3,7 @@ name = "example-hello-world" version = "0.1.0" publish = false description = "Hello World - Cot example." -edition = "2021" +edition = "2024" [dependencies] cot = { path = "../../cot" } diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml index 9293b5b2..f2b1d91a 100644 --- a/examples/json/Cargo.toml +++ b/examples/json/Cargo.toml @@ -3,7 +3,7 @@ name = "example-json" version = "0.1.0" publish = false description = "JSON - Cot example." -edition = "2021" +edition = "2024" [dependencies] cot = { path = "../../cot" } diff --git a/examples/sessions/Cargo.toml b/examples/sessions/Cargo.toml index 58997474..5e38ceaf 100644 --- a/examples/sessions/Cargo.toml +++ b/examples/sessions/Cargo.toml @@ -3,7 +3,7 @@ name = "example-sessions" version = "0.1.0" publish = false description = "Sessions - Cot example." -edition = "2021" +edition = "2024" [dependencies] cot = { path = "../../cot" } diff --git a/examples/sessions/src/main.rs b/examples/sessions/src/main.rs index 814e0017..e4dee985 100644 --- a/examples/sessions/src/main.rs +++ b/examples/sessions/src/main.rs @@ -7,7 +7,7 @@ use cot::request::Request; use cot::response::{Response, ResponseExt}; use cot::router::{Route, Router, Urls}; use cot::session::Session; -use cot::{reverse_redirect, App, AppBuilder, Body, BoxedHandler, Project, StatusCode}; +use cot::{App, AppBuilder, Body, BoxedHandler, Project, StatusCode, reverse_redirect}; use rinja::Template; #[derive(Debug, Template)] diff --git a/examples/todo-list/Cargo.toml b/examples/todo-list/Cargo.toml index d68e3ff7..49ec09c8 100644 --- a/examples/todo-list/Cargo.toml +++ b/examples/todo-list/Cargo.toml @@ -3,7 +3,7 @@ name = "example-todo-list" version = "0.1.0" publish = false description = "TODO List - Cot example." -edition = "2021" +edition = "2024" [dependencies] cot = { path = "../../cot" } diff --git a/examples/todo-list/src/main.rs b/examples/todo-list/src/main.rs index 2ae33b18..bdb89507 100644 --- a/examples/todo-list/src/main.rs +++ b/examples/todo-list/src/main.rs @@ -4,14 +4,14 @@ use cot::auth::db::DatabaseUserApp; use cot::cli::CliMetadata; use cot::config::{DatabaseConfig, ProjectConfig}; use cot::db::migrations::SyncDynMigration; -use cot::db::{model, query, Auto, Model}; +use cot::db::{Auto, Model, model, query}; use cot::form::Form; use cot::project::{MiddlewareContext, RegisterAppsContext}; use cot::request::extractors::{Path, RequestDb, RequestForm}; use cot::response::{Response, ResponseExt}; use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; -use cot::{reverse_redirect, App, AppBuilder, Body, BoxedHandler, Project, StatusCode}; +use cot::{App, AppBuilder, Body, BoxedHandler, Project, StatusCode, reverse_redirect}; use rinja::Template; #[derive(Debug, Clone)]