Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bee7a7940 | ||
|
|
1bd3e17672 |
@@ -0,0 +1,60 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Pet sitting web service for managing clients and bookings. The owner uses the site to:
|
||||||
|
- Receive and manage client requests (leads) from the website
|
||||||
|
- Schedule calls and visits with clients
|
||||||
|
- Upload photos/videos of pets for remote viewing by clients (public media page via unique token)
|
||||||
|
- Get Telegram notifications about new requests
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Language:** Rust (edition 2024)
|
||||||
|
- **Web framework:** [Cot](https://github.com/cot-rs/cot) - Rust web framework (Django-like), local path `../cot/cot`
|
||||||
|
- **Database:** PostgreSQL (via Cot ORM)
|
||||||
|
- **Notifications:** Telegram Bot API
|
||||||
|
|
||||||
|
## Build & Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build # build
|
||||||
|
cargo run # run dev server at http://127.0.0.1:8000
|
||||||
|
cargo test # run all tests
|
||||||
|
cargo test <name> # run a single test by name
|
||||||
|
cargo clippy # lint
|
||||||
|
cargo fmt --check # check formatting
|
||||||
|
cot migration make # generate migrations from model changes (requires cot-cli)
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `WEB_PETTING_DATABASE_URL` (or `DATABASE_URL`) before running the app or migrations. Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
WEB_PETTING_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/web_petting cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Monolithic Cot web app with a single PostgreSQL database.
|
||||||
|
|
||||||
|
- `src/main.rs` - project/app setup, router, config
|
||||||
|
- `src/models.rs` - all database models (Lead, Client, Visit, Media, User, Setting)
|
||||||
|
- `src/migrations.rs` - migration registry
|
||||||
|
- `src/migrations/` - migration files
|
||||||
|
|
||||||
|
## Database Design Principles
|
||||||
|
|
||||||
|
- **Soft-delete everywhere:** records are never physically deleted, only status changes (e.g. `active` -> `archived`, `new` -> `rejected`). This ensures data can always be recovered.
|
||||||
|
- **Status fields** are stored as `String` with enum-like values defined in `models.rs`.
|
||||||
|
- **Foreign keys** use `cot::db::ForeignKey<T>` with `Restrict` on delete/update.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
- **Lead** (`new`/`in_progress`/`converted`/`rejected`) - public form submission; links to Client when converted
|
||||||
|
- **Client** (`active`/`archived`) - confirmed client with `media_token` for public media page
|
||||||
|
- **Visit** (`scheduled`/`completed`/`cancelled`/`deleted`) - pet sitting session, belongs to Client and User
|
||||||
|
- **Media** (`active`/`archived`) - photo/video, belongs to Client, optionally to Visit
|
||||||
|
- **User** (`active`/`archived`) - admin accounts (supports multiple admins)
|
||||||
|
- **Setting** - global key-value config (telegram_bot_token, telegram_chat_id, etc.)
|
||||||
@@ -13,30 +13,36 @@ Pet sitting web service for managing clients and bookings. The owner uses the si
|
|||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Language:** Rust (edition 2024)
|
- **Language:** Rust (edition 2024)
|
||||||
- **Web framework:** [Cot](https://github.com/cot-rs/cot) — Rust web framework (Django-like), local path `../cot/cot`
|
- **Web framework:** [Cot](https://github.com/cot-rs/cot) - Rust web framework (Django-like), local path `../cot/cot`
|
||||||
- **Database:** SQLite (via Cot ORM), file `db.sqlite3`
|
- **Database:** PostgreSQL (via Cot ORM)
|
||||||
- **Notifications:** Telegram Bot API
|
- **Notifications:** Telegram Bot API
|
||||||
|
|
||||||
## Build & Run
|
## Build & Run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo build # build
|
cargo build # build
|
||||||
cargo run # run dev server at http://127.0.0.1:8000
|
cargo run # run dev server at http://127.0.0.1:8000
|
||||||
cargo test # run all tests
|
cargo test # run all tests
|
||||||
cargo test <name> # run a single test by name
|
cargo test <name> # run a single test by name
|
||||||
cargo clippy # lint
|
cargo clippy # lint
|
||||||
cargo fmt --check # check formatting
|
cargo fmt --check # check formatting
|
||||||
cot migration make # generate migrations from model changes (requires cot-cli)
|
cot migration make # generate migrations from model changes (requires cot-cli)
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `WEB_PETTING_DATABASE_URL` (or `DATABASE_URL`) before running the app or migrations. Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
WEB_PETTING_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/web_petting cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Monolithic Cot web app with a single SQLite database.
|
Monolithic Cot web app with a single PostgreSQL database.
|
||||||
|
|
||||||
- `src/main.rs` — project/app setup, router, config
|
- `src/main.rs` - project/app setup, router, config
|
||||||
- `src/models.rs` — all database models (Lead, Client, Visit, Media, User, Setting)
|
- `src/models.rs` - all database models (Lead, Client, Visit, Media, User, Setting)
|
||||||
- `src/migrations.rs` — migration registry (auto-generated by `cot migration make`)
|
- `src/migrations.rs` - migration registry
|
||||||
- `src/migrations/` — migration files (auto-generated)
|
- `src/migrations/` - migration files
|
||||||
|
|
||||||
## Database Design Principles
|
## Database Design Principles
|
||||||
|
|
||||||
@@ -46,9 +52,9 @@ Monolithic Cot web app with a single SQLite database.
|
|||||||
|
|
||||||
## Data Model
|
## Data Model
|
||||||
|
|
||||||
- **Lead** (`new`/`in_progress`/`converted`/`rejected`) — public form submission; links to Client when converted
|
- **Lead** (`new`/`in_progress`/`converted`/`rejected`) - public form submission; links to Client when converted
|
||||||
- **Client** (`active`/`archived`) — confirmed client with `media_token` for public media page
|
- **Client** (`active`/`archived`) - confirmed client with `media_token` for public media page
|
||||||
- **Visit** (`scheduled`/`completed`/`cancelled`) — pet sitting session, belongs to Client
|
- **Visit** (`scheduled`/`completed`/`cancelled`/`deleted`) - pet sitting session, belongs to Client and User
|
||||||
- **Media** (`active`/`archived`) — photo/video, belongs to Client, optionally to Visit
|
- **Media** (`active`/`archived`) - photo/video, belongs to Client, optionally to Visit
|
||||||
- **User** (`active`/`archived`) — admin accounts (supports multiple admins)
|
- **User** (`active`/`archived`) - admin accounts (supports multiple admins)
|
||||||
- **Setting** — global key-value config (telegram_bot_token, telegram_chat_id, etc.)
|
- **Setting** - global key-value config (telegram_bot_token, telegram_chat_id, etc.)
|
||||||
|
|||||||
Generated
+1
-2
@@ -1617,7 +1617,6 @@ version = "0.30.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
@@ -3467,7 +3466,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "0.1.14"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "0.1.15"
|
version = "1.0.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cot = { version = "0.6.0", features = ["sqlite"] }
|
cot = { version = "0.6.0", default-features = false, features = ["postgres", "json"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = "0.10"
|
chrono-tz = "0.10"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ RUN cargo build --release
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
ENV WEB_PETTING_UPLOAD_DIR=/data/uploads
|
||||||
COPY --from=builder /app/target/release/web-petting /usr/local/bin/web-petting
|
COPY --from=builder /app/target/release/web-petting /usr/local/bin/web-petting
|
||||||
COPY static /app/static
|
COPY static /app/static
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
+48
-19
@@ -151,14 +151,14 @@ async fn save_uploaded_image(
|
|||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> cot::Result<String> {
|
) -> cot::Result<String> {
|
||||||
if let Some(encoded) = transcode_uploaded_image(data, ext)? {
|
if let Some(encoded) = transcode_uploaded_image(data, ext)? {
|
||||||
let path = format!("{}/{}.jpg", upload_dir, file_id);
|
let path = crate::uploads::join_db_path(upload_dir, &format!("{file_id}.jpg"));
|
||||||
tokio::fs::write(&path, &encoded)
|
crate::uploads::write_db_file(&path, &encoded)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
} else {
|
} else {
|
||||||
let path = format!("{}/{}.{}", upload_dir, file_id, ext);
|
let path = crate::uploads::join_db_path(upload_dir, &format!("{file_id}.{ext}"));
|
||||||
tokio::fs::write(&path, data)
|
crate::uploads::write_db_file(&path, data)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
@@ -2140,8 +2140,8 @@ async fn media_upload_submit(
|
|||||||
futures::stream::once(async move { Result::<_, std::convert::Infallible>::Ok(bytes) });
|
futures::stream::once(async move { Result::<_, std::convert::Infallible>::Ok(bytes) });
|
||||||
let mut multipart = multer::Multipart::new(stream, boundary);
|
let mut multipart = multer::Multipart::new(stream, boundary);
|
||||||
|
|
||||||
let upload_dir = format!("uploads/{}/{}", client_id, visit_id);
|
let upload_dir = crate::uploads::media_dir(client_id, visit_id);
|
||||||
tokio::fs::create_dir_all(&upload_dir)
|
crate::uploads::create_logical_dir(&upload_dir)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
|
|
||||||
@@ -2193,8 +2193,8 @@ async fn media_upload_submit(
|
|||||||
let file_path = if file_type == "photo" {
|
let file_path = if file_type == "photo" {
|
||||||
save_uploaded_image(&upload_dir, file_id, &ext, &data).await?
|
save_uploaded_image(&upload_dir, file_id, &ext, &data).await?
|
||||||
} else {
|
} else {
|
||||||
let path = format!("{}/{}.{}", upload_dir, file_id, ext);
|
let path = crate::uploads::join_db_path(&upload_dir, &format!("{file_id}.{ext}"));
|
||||||
tokio::fs::write(&path, &data)
|
crate::uploads::write_db_file(&path, &data)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
path
|
path
|
||||||
@@ -2251,7 +2251,16 @@ async fn media_delete(
|
|||||||
let file_path = m.file_path.clone();
|
let file_path = m.file_path.clone();
|
||||||
m.status = "archived".to_string();
|
m.status = "archived".to_string();
|
||||||
m.save(&db).await?;
|
m.save(&db).await?;
|
||||||
let _ = tokio::fs::remove_file(&file_path).await;
|
if let Err(err) = crate::uploads::remove_db_file(&file_path).await {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "uploads",
|
||||||
|
media_id,
|
||||||
|
db_path = %file_path,
|
||||||
|
resolved_path = %crate::uploads::resolved_display_path(&file_path),
|
||||||
|
error = %err,
|
||||||
|
"failed to remove uploaded file"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let redirect_url = referer
|
let redirect_url = referer
|
||||||
.filter(|r| r.contains("/schedule/") && r.contains("/edit"))
|
.filter(|r| r.contains("/schedule/") && r.contains("/edit"))
|
||||||
@@ -2276,7 +2285,7 @@ async fn serve_upload(
|
|||||||
None => return Html::new("404").into_response(),
|
None => return Html::new("404").into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match tokio::fs::read(&media.file_path).await {
|
match crate::uploads::read_db_file(&media.file_path).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let content_type = match media.file_path.rsplit('.').next().unwrap_or("") {
|
let content_type = match media.file_path.rsplit('.').next().unwrap_or("") {
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
"jpg" | "jpeg" => "image/jpeg",
|
||||||
@@ -2296,7 +2305,17 @@ async fn serve_upload(
|
|||||||
.insert("content-type", content_type.parse().unwrap());
|
.insert("content-type", content_type.parse().unwrap());
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
Err(_) => Html::new("404").into_response(),
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "uploads",
|
||||||
|
media_id,
|
||||||
|
db_path = %media.file_path,
|
||||||
|
resolved_path = %crate::uploads::resolved_display_path(&media.file_path),
|
||||||
|
error = %err,
|
||||||
|
"uploaded file is missing or unreadable"
|
||||||
|
);
|
||||||
|
Html::new("404").into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2403,12 +2422,12 @@ async fn testimonial_add(
|
|||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let upload_dir = "uploads/testimonials";
|
let upload_dir = crate::uploads::testimonials_dir();
|
||||||
tokio::fs::create_dir_all(upload_dir)
|
crate::uploads::create_logical_dir(&upload_dir)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
let file_id = uuid::Uuid::new_v4();
|
let file_id = uuid::Uuid::new_v4();
|
||||||
let path = save_uploaded_image(upload_dir, file_id, &ext, &data).await?;
|
let path = save_uploaded_image(&upload_dir, file_id, &ext, &data).await?;
|
||||||
image_path = Some(path);
|
image_path = Some(path);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -2551,12 +2570,12 @@ async fn testimonial_edit(
|
|||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let upload_dir = "uploads/testimonials";
|
let upload_dir = crate::uploads::testimonials_dir();
|
||||||
tokio::fs::create_dir_all(upload_dir)
|
crate::uploads::create_logical_dir(&upload_dir)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
let file_id = uuid::Uuid::new_v4();
|
let file_id = uuid::Uuid::new_v4();
|
||||||
let path = save_uploaded_image(upload_dir, file_id, &ext, &data).await?;
|
let path = save_uploaded_image(&upload_dir, file_id, &ext, &data).await?;
|
||||||
new_image_path = Some(path);
|
new_image_path = Some(path);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -2597,7 +2616,7 @@ async fn serve_testimonial_image(
|
|||||||
Some(p) => p.clone(),
|
Some(p) => p.clone(),
|
||||||
None => return Html::new("404").into_response(),
|
None => return Html::new("404").into_response(),
|
||||||
};
|
};
|
||||||
match tokio::fs::read(&path).await {
|
match crate::uploads::read_db_file(&path).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let content_type = match path.rsplit('.').next().unwrap_or("") {
|
let content_type = match path.rsplit('.').next().unwrap_or("") {
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
"jpg" | "jpeg" => "image/jpeg",
|
||||||
@@ -2612,7 +2631,17 @@ async fn serve_testimonial_image(
|
|||||||
.insert("content-type", content_type.parse().unwrap());
|
.insert("content-type", content_type.parse().unwrap());
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
Err(_) => Html::new("404").into_response(),
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "uploads",
|
||||||
|
testimonial_id = id,
|
||||||
|
db_path = %path,
|
||||||
|
resolved_path = %crate::uploads::resolved_display_path(&path),
|
||||||
|
error = %err,
|
||||||
|
"testimonial image is missing or unreadable"
|
||||||
|
);
|
||||||
|
Html::new("404").into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-8
@@ -6,6 +6,7 @@ mod public;
|
|||||||
mod telegram;
|
mod telegram;
|
||||||
mod turnstile;
|
mod turnstile;
|
||||||
mod tz;
|
mod tz;
|
||||||
|
mod uploads;
|
||||||
|
|
||||||
use tracing_subscriber;
|
use tracing_subscriber;
|
||||||
|
|
||||||
@@ -69,6 +70,12 @@ fn debug_enabled(config_name: &str) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn database_url() -> String {
|
||||||
|
std::env::var("WEB_PETTING_DATABASE_URL")
|
||||||
|
.or_else(|_| std::env::var("DATABASE_URL"))
|
||||||
|
.unwrap_or_else(|_| "postgresql://postgres:postgres@localhost:5432/web_petting".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
impl Project for PettingProject {
|
impl Project for PettingProject {
|
||||||
fn cli_metadata(&self) -> CliMetadata {
|
fn cli_metadata(&self) -> CliMetadata {
|
||||||
cot::cli::metadata!()
|
cot::cli::metadata!()
|
||||||
@@ -77,11 +84,7 @@ impl Project for PettingProject {
|
|||||||
fn config(&self, config_name: &str) -> cot::Result<ProjectConfig> {
|
fn config(&self, config_name: &str) -> cot::Result<ProjectConfig> {
|
||||||
Ok(ProjectConfig::builder()
|
Ok(ProjectConfig::builder()
|
||||||
.debug(debug_enabled(config_name))
|
.debug(debug_enabled(config_name))
|
||||||
.database(
|
.database(DatabaseConfig::builder().url(database_url()).build())
|
||||||
DatabaseConfig::builder()
|
|
||||||
.url("sqlite://db.sqlite3?mode=rwc")
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.middlewares(
|
.middlewares(
|
||||||
MiddlewareConfig::builder()
|
MiddlewareConfig::builder()
|
||||||
.session(
|
.session(
|
||||||
@@ -121,8 +124,6 @@ impl Project for PettingProject {
|
|||||||
fn main() -> impl Project {
|
fn main() -> impl Project {
|
||||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
||||||
let _ = tracing_subscriber::fmt()
|
let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init();
|
||||||
.with_env_filter(filter)
|
|
||||||
.try_init();
|
|
||||||
PettingProject
|
PettingProject
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-14
@@ -1,19 +1,7 @@
|
|||||||
//! List of migrations for the current app.
|
//! List of migrations for the current app.
|
||||||
//!
|
//!
|
||||||
//! Generated by cot CLI 0.6.0 on 2026-04-29 10:36:47+00:00
|
//! Squashed for the PostgreSQL migration on 2026-07-11.
|
||||||
|
|
||||||
pub mod m_0001_initial;
|
pub mod m_0001_initial;
|
||||||
pub mod m_0002_visit_schedule;
|
|
||||||
pub mod m_0003_visit_feedback;
|
|
||||||
pub mod m_0004_visit_public_notes;
|
|
||||||
pub mod m_0005_testimonials;
|
|
||||||
pub mod m_0006_user_telegram;
|
|
||||||
/// The list of migrations for current app.
|
/// The list of migrations for current app.
|
||||||
pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[
|
pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[&m_0001_initial::Migration];
|
||||||
&m_0001_initial::Migration,
|
|
||||||
&m_0002_visit_schedule::Migration,
|
|
||||||
&m_0003_visit_feedback::Migration,
|
|
||||||
&m_0004_visit_public_notes::Migration,
|
|
||||||
&m_0005_testimonials::Migration,
|
|
||||||
&m_0006_user_telegram::Migration,
|
|
||||||
];
|
|
||||||
|
|||||||
+376
-459
@@ -1,7 +1,8 @@
|
|||||||
//! Generated by cot CLI 0.6.0 on 2026-04-29 10:36:47+00:00
|
//! Initial PostgreSQL schema for the current data model.
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub(super) struct Migration;
|
pub(super) struct Migration;
|
||||||
|
|
||||||
impl ::cot::db::migrations::Migration for Migration {
|
impl ::cot::db::migrations::Migration for Migration {
|
||||||
const APP_NAME: &'static str = "web-petting";
|
const APP_NAME: &'static str = "web-petting";
|
||||||
const MIGRATION_NAME: &'static str = "m_0001_initial";
|
const MIGRATION_NAME: &'static str = "m_0001_initial";
|
||||||
@@ -9,479 +10,395 @@ impl ::cot::db::migrations::Migration for Migration {
|
|||||||
const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[
|
const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[
|
||||||
::cot::db::migrations::Operation::create_model()
|
::cot::db::migrations::Operation::create_model()
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__user"))
|
.table_name(::cot::db::Identifier::new("web_petting__user"))
|
||||||
.fields(
|
.fields(&[
|
||||||
&[
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("id"),
|
||||||
::cot::db::Identifier::new("id"),
|
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.auto()
|
||||||
.auto()
|
.primary_key()
|
||||||
.primary_key()
|
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
::cot::db::migrations::Field::new(
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::Identifier::new("login"),
|
||||||
),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("login"),
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
.unique(),
|
||||||
)
|
::cot::db::migrations::Field::new(
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
::cot::db::Identifier::new("password_hash"),
|
||||||
.unique(),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("password_hash"),
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("display_name"),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("display_name"),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("telegram_chat_id"),
|
||||||
.set_null(
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
)
|
||||||
),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::Identifier::new("status"),
|
::cot::db::Identifier::new("telegram_notifications"),
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
<Option<bool> as ::cot::db::DatabaseField>::TYPE,
|
||||||
)
|
)
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
.set_null(<Option<bool> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::Identifier::new("created_at"),
|
::cot::db::Identifier::new("status"),
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
)
|
)
|
||||||
.set_null(
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::migrations::Field::new(
|
||||||
),
|
::cot::db::Identifier::new("created_at"),
|
||||||
::cot::db::migrations::Field::new(
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::Identifier::new("updated_at"),
|
)
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
)
|
::cot::db::migrations::Field::new(
|
||||||
.set_null(
|
::cot::db::Identifier::new("updated_at"),
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
),
|
)
|
||||||
],
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
)
|
])
|
||||||
.build(),
|
.build(),
|
||||||
::cot::db::migrations::Operation::create_model()
|
::cot::db::migrations::Operation::create_model()
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__setting"))
|
.table_name(::cot::db::Identifier::new("web_petting__setting"))
|
||||||
.fields(
|
.fields(&[
|
||||||
&[
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("id"),
|
||||||
::cot::db::Identifier::new("id"),
|
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.auto()
|
||||||
.auto()
|
.primary_key()
|
||||||
.primary_key()
|
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
::cot::db::migrations::Field::new(
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::Identifier::new("key"),
|
||||||
),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("key"),
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
.unique(),
|
||||||
)
|
::cot::db::migrations::Field::new(
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
::cot::db::Identifier::new("value"),
|
||||||
.unique(),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("value"),
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("updated_at"),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("updated_at"),
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
])
|
||||||
)
|
|
||||||
.set_null(
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.build(),
|
.build(),
|
||||||
::cot::db::migrations::Operation::create_model()
|
::cot::db::migrations::Operation::create_model()
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__client"))
|
.table_name(::cot::db::Identifier::new("web_petting__client"))
|
||||||
.fields(
|
.fields(&[
|
||||||
&[
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("id"),
|
||||||
::cot::db::Identifier::new("id"),
|
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.auto()
|
||||||
.auto()
|
.primary_key()
|
||||||
.primary_key()
|
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
::cot::db::migrations::Field::new(
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::Identifier::new("name"),
|
||||||
),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("name"),
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("phone"),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("phone"),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("email"),
|
||||||
.set_null(
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
)
|
||||||
),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::Identifier::new("email"),
|
::cot::db::Identifier::new("address"),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
)
|
)
|
||||||
.set_null(
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::migrations::Field::new(
|
||||||
),
|
::cot::db::Identifier::new("notes"),
|
||||||
::cot::db::migrations::Field::new(
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::Identifier::new("address"),
|
)
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
)
|
::cot::db::migrations::Field::new(
|
||||||
.set_null(
|
::cot::db::Identifier::new("media_token"),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
),
|
)
|
||||||
::cot::db::migrations::Field::new(
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
||||||
::cot::db::Identifier::new("notes"),
|
.unique(),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("color"),
|
||||||
.set_null(
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
)
|
||||||
),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::Identifier::new("media_token"),
|
::cot::db::Identifier::new("status"),
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
)
|
)
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.unique(),
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("created_at"),
|
||||||
::cot::db::Identifier::new("status"),
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("updated_at"),
|
||||||
::cot::db::Identifier::new("created_at"),
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
])
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
.build(),
|
||||||
),
|
::cot::db::migrations::Operation::create_model()
|
||||||
::cot::db::migrations::Field::new(
|
.table_name(::cot::db::Identifier::new("web_petting__testimonial"))
|
||||||
::cot::db::Identifier::new("updated_at"),
|
.fields(&[
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("id"),
|
||||||
.set_null(
|
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
)
|
||||||
),
|
.auto()
|
||||||
],
|
.primary_key()
|
||||||
)
|
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("text"),
|
||||||
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("author_note"),
|
||||||
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("image_path"),
|
||||||
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("status"),
|
||||||
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("sort_order"),
|
||||||
|
<i32 as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<i32 as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("created_at"),
|
||||||
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
])
|
||||||
.build(),
|
.build(),
|
||||||
::cot::db::migrations::Operation::create_model()
|
::cot::db::migrations::Operation::create_model()
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
||||||
.fields(
|
.fields(&[
|
||||||
&[
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("id"),
|
||||||
::cot::db::Identifier::new("id"),
|
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.auto()
|
||||||
.auto()
|
.primary_key()
|
||||||
.primary_key()
|
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
::cot::db::migrations::Field::new(
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::Identifier::new("client_id"),
|
||||||
),
|
<cot::db::ForeignKey<crate::models::Client> as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("client_id"),
|
.foreign_key(
|
||||||
<cot::db::ForeignKey<
|
<crate::models::Client as ::cot::db::Model>::TABLE_NAME,
|
||||||
crate::models::Client,
|
<crate::models::Client as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
||||||
> as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
||||||
)
|
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
||||||
.foreign_key(
|
)
|
||||||
<crate::models::Client as ::cot::db::Model>::TABLE_NAME,
|
.set_null(
|
||||||
<crate::models::Client as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
<cot::db::ForeignKey<crate::models::Client> as ::cot::db::DatabaseField>::NULLABLE,
|
||||||
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
),
|
||||||
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("user_id"),
|
||||||
.set_null(
|
<cot::db::ForeignKey<crate::models::User> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::ForeignKey<
|
)
|
||||||
crate::models::Client,
|
.foreign_key(
|
||||||
> as ::cot::db::DatabaseField>::NULLABLE,
|
<crate::models::User as ::cot::db::Model>::TABLE_NAME,
|
||||||
),
|
<crate::models::User as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
||||||
::cot::db::Identifier::new("scheduled_at"),
|
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(
|
||||||
.set_null(
|
<cot::db::ForeignKey<crate::models::User> as ::cot::db::DatabaseField>::NULLABLE,
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
),
|
||||||
),
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("visit_date"),
|
||||||
::cot::db::Identifier::new("duration_minutes"),
|
<chrono::NaiveDate as ::cot::db::DatabaseField>::TYPE,
|
||||||
<Option<i32> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(<chrono::NaiveDate as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(<Option<i32> as ::cot::db::DatabaseField>::NULLABLE),
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("time_start"),
|
||||||
::cot::db::Identifier::new("notes"),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
::cot::db::migrations::Field::new(
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::Identifier::new("time_end"),
|
||||||
),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("status"),
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("notes"),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("created_at"),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("public_notes"),
|
||||||
.set_null(
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
)
|
||||||
),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::Identifier::new("updated_at"),
|
::cot::db::Identifier::new("client_feedback"),
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
)
|
)
|
||||||
.set_null(
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::migrations::Field::new(
|
||||||
),
|
::cot::db::Identifier::new("status"),
|
||||||
],
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
)
|
)
|
||||||
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("created_at"),
|
||||||
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
::cot::db::migrations::Field::new(
|
||||||
|
::cot::db::Identifier::new("updated_at"),
|
||||||
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
|
)
|
||||||
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
|
])
|
||||||
.build(),
|
.build(),
|
||||||
::cot::db::migrations::Operation::create_model()
|
::cot::db::migrations::Operation::create_model()
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__media"))
|
.table_name(::cot::db::Identifier::new("web_petting__media"))
|
||||||
.fields(
|
.fields(&[
|
||||||
&[
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("id"),
|
||||||
::cot::db::Identifier::new("id"),
|
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.auto()
|
||||||
.auto()
|
.primary_key()
|
||||||
.primary_key()
|
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
::cot::db::migrations::Field::new(
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::Identifier::new("client_id"),
|
||||||
),
|
<cot::db::ForeignKey<crate::models::Client> as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("client_id"),
|
.foreign_key(
|
||||||
<cot::db::ForeignKey<
|
<crate::models::Client as ::cot::db::Model>::TABLE_NAME,
|
||||||
crate::models::Client,
|
<crate::models::Client as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
||||||
> as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
||||||
)
|
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
||||||
.foreign_key(
|
)
|
||||||
<crate::models::Client as ::cot::db::Model>::TABLE_NAME,
|
.set_null(
|
||||||
<crate::models::Client as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
<cot::db::ForeignKey<crate::models::Client> as ::cot::db::DatabaseField>::NULLABLE,
|
||||||
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
),
|
||||||
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("visit_id"),
|
||||||
.set_null(
|
<Option<cot::db::ForeignKey<crate::models::Visit>> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::ForeignKey<
|
)
|
||||||
crate::models::Client,
|
.foreign_key(
|
||||||
> as ::cot::db::DatabaseField>::NULLABLE,
|
<crate::models::Visit as ::cot::db::Model>::TABLE_NAME,
|
||||||
),
|
<crate::models::Visit as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
||||||
::cot::db::Identifier::new("visit_id"),
|
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
||||||
<Option<
|
)
|
||||||
cot::db::ForeignKey<crate::models::Visit>,
|
.set_null(
|
||||||
> as ::cot::db::DatabaseField>::TYPE,
|
<Option<cot::db::ForeignKey<crate::models::Visit>> as ::cot::db::DatabaseField>::NULLABLE,
|
||||||
)
|
),
|
||||||
.foreign_key(
|
::cot::db::migrations::Field::new(
|
||||||
<crate::models::Visit as ::cot::db::Model>::TABLE_NAME,
|
::cot::db::Identifier::new("file_path"),
|
||||||
<crate::models::Visit as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
)
|
||||||
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
)
|
::cot::db::migrations::Field::new(
|
||||||
.set_null(
|
::cot::db::Identifier::new("file_type"),
|
||||||
<Option<
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
cot::db::ForeignKey<crate::models::Visit>,
|
)
|
||||||
> as ::cot::db::DatabaseField>::NULLABLE,
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
),
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("caption"),
|
||||||
::cot::db::Identifier::new("file_path"),
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("status"),
|
||||||
::cot::db::Identifier::new("file_type"),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("created_at"),
|
||||||
::cot::db::Identifier::new("caption"),
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
])
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
|
||||||
),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("status"),
|
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("created_at"),
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.build(),
|
.build(),
|
||||||
::cot::db::migrations::Operation::create_model()
|
::cot::db::migrations::Operation::create_model()
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__lead"))
|
.table_name(::cot::db::Identifier::new("web_petting__lead"))
|
||||||
.fields(
|
.fields(&[
|
||||||
&[
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::Identifier::new("id"),
|
||||||
::cot::db::Identifier::new("id"),
|
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
)
|
||||||
)
|
.auto()
|
||||||
.auto()
|
.primary_key()
|
||||||
.primary_key()
|
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
.set_null(
|
::cot::db::migrations::Field::new(
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::Identifier::new("name"),
|
||||||
),
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("name"),
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("phone"),
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("phone"),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("email"),
|
||||||
.set_null(
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
)
|
||||||
),
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
::cot::db::migrations::Field::new(
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::Identifier::new("email"),
|
::cot::db::Identifier::new("comment"),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
||||||
)
|
)
|
||||||
.set_null(
|
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
::cot::db::migrations::Field::new(
|
||||||
),
|
::cot::db::Identifier::new("status"),
|
||||||
::cot::db::migrations::Field::new(
|
<String as ::cot::db::DatabaseField>::TYPE,
|
||||||
::cot::db::Identifier::new("comment"),
|
)
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
)
|
::cot::db::migrations::Field::new(
|
||||||
.set_null(
|
::cot::db::Identifier::new("client_id"),
|
||||||
<Option<String> as ::cot::db::DatabaseField>::NULLABLE,
|
<Option<cot::db::ForeignKey<crate::models::Client>> as ::cot::db::DatabaseField>::TYPE,
|
||||||
),
|
)
|
||||||
::cot::db::migrations::Field::new(
|
.foreign_key(
|
||||||
::cot::db::Identifier::new("status"),
|
<crate::models::Client as ::cot::db::Model>::TABLE_NAME,
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
<crate::models::Client as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
||||||
)
|
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
||||||
::cot::db::migrations::Field::new(
|
)
|
||||||
::cot::db::Identifier::new("client_id"),
|
.set_null(
|
||||||
<Option<
|
<Option<cot::db::ForeignKey<crate::models::Client>> as ::cot::db::DatabaseField>::NULLABLE,
|
||||||
cot::db::ForeignKey<crate::models::Client>,
|
),
|
||||||
> as ::cot::db::DatabaseField>::TYPE,
|
::cot::db::migrations::Field::new(
|
||||||
)
|
::cot::db::Identifier::new("created_at"),
|
||||||
.foreign_key(
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
<crate::models::Client as ::cot::db::Model>::TABLE_NAME,
|
)
|
||||||
<crate::models::Client as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
::cot::db::migrations::Field::new(
|
||||||
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
::cot::db::Identifier::new("updated_at"),
|
||||||
)
|
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
||||||
.set_null(
|
)
|
||||||
<Option<
|
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
||||||
cot::db::ForeignKey<crate::models::Client>,
|
])
|
||||||
> as ::cot::db::DatabaseField>::NULLABLE,
|
|
||||||
),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("created_at"),
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
|
||||||
),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("updated_at"),
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.build(),
|
.build(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(::core::fmt::Debug)]
|
|
||||||
#[::cot::db::model(model_type = "migration")]
|
|
||||||
struct _Client {
|
|
||||||
#[model(primary_key)]
|
|
||||||
pub id: cot::db::Auto<i64>,
|
|
||||||
pub name: String,
|
|
||||||
pub phone: Option<String>,
|
|
||||||
pub email: Option<String>,
|
|
||||||
pub address: Option<String>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
/// Unique token for the public media page (client views photos/videos here).
|
|
||||||
#[model(unique)]
|
|
||||||
pub media_token: String,
|
|
||||||
/// active | archived
|
|
||||||
pub status: String,
|
|
||||||
pub created_at: chrono::NaiveDateTime,
|
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
|
||||||
}
|
|
||||||
#[derive(::core::fmt::Debug)]
|
|
||||||
#[::cot::db::model(model_type = "migration")]
|
|
||||||
struct _Lead {
|
|
||||||
#[model(primary_key)]
|
|
||||||
pub id: cot::db::Auto<i64>,
|
|
||||||
pub name: String,
|
|
||||||
pub phone: Option<String>,
|
|
||||||
pub email: Option<String>,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
/// new | in_progress | converted | rejected
|
|
||||||
pub status: String,
|
|
||||||
pub client_id: Option<cot::db::ForeignKey<crate::models::Client>>,
|
|
||||||
pub created_at: chrono::NaiveDateTime,
|
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
|
||||||
}
|
|
||||||
#[derive(::core::fmt::Debug)]
|
|
||||||
#[::cot::db::model(model_type = "migration")]
|
|
||||||
struct _Media {
|
|
||||||
#[model(primary_key)]
|
|
||||||
pub id: cot::db::Auto<i64>,
|
|
||||||
pub client_id: cot::db::ForeignKey<crate::models::Client>,
|
|
||||||
pub visit_id: Option<cot::db::ForeignKey<crate::models::Visit>>,
|
|
||||||
pub file_path: String,
|
|
||||||
/// photo | video
|
|
||||||
pub file_type: String,
|
|
||||||
pub caption: Option<String>,
|
|
||||||
/// active | archived
|
|
||||||
pub status: String,
|
|
||||||
pub created_at: chrono::NaiveDateTime,
|
|
||||||
}
|
|
||||||
#[derive(::core::fmt::Debug)]
|
|
||||||
#[::cot::db::model(model_type = "migration")]
|
|
||||||
struct _Setting {
|
|
||||||
#[model(primary_key)]
|
|
||||||
pub id: cot::db::Auto<i64>,
|
|
||||||
#[model(unique)]
|
|
||||||
pub key: String,
|
|
||||||
pub value: String,
|
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
|
||||||
}
|
|
||||||
#[derive(::core::fmt::Debug)]
|
|
||||||
#[::cot::db::model(model_type = "migration")]
|
|
||||||
struct _User {
|
|
||||||
#[model(primary_key)]
|
|
||||||
pub id: cot::db::Auto<i64>,
|
|
||||||
#[model(unique)]
|
|
||||||
pub login: String,
|
|
||||||
pub password_hash: String,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
/// active | archived
|
|
||||||
pub status: String,
|
|
||||||
pub created_at: chrono::NaiveDateTime,
|
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
|
||||||
}
|
|
||||||
#[derive(::core::fmt::Debug)]
|
|
||||||
#[::cot::db::model(model_type = "migration")]
|
|
||||||
struct _Visit {
|
|
||||||
#[model(primary_key)]
|
|
||||||
pub id: cot::db::Auto<i64>,
|
|
||||||
pub client_id: cot::db::ForeignKey<crate::models::Client>,
|
|
||||||
pub scheduled_at: chrono::NaiveDateTime,
|
|
||||||
pub duration_minutes: Option<i32>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
/// scheduled | completed | cancelled
|
|
||||||
pub status: String,
|
|
||||||
pub created_at: chrono::NaiveDateTime,
|
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
//! Migration: update Visit model for scheduling + add Client.color
|
|
||||||
//! Visit: Remove scheduled_at, duration_minutes; Add user_id, visit_date, time_start, time_end
|
|
||||||
//! Client: Add color
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub(super) struct Migration;
|
|
||||||
impl ::cot::db::migrations::Migration for Migration {
|
|
||||||
const APP_NAME: &'static str = "web-petting";
|
|
||||||
const MIGRATION_NAME: &'static str = "m_0002_visit_schedule";
|
|
||||||
const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] =
|
|
||||||
&[::cot::db::migrations::MigrationDependency::migration(
|
|
||||||
"web-petting",
|
|
||||||
"m_0001_initial",
|
|
||||||
)];
|
|
||||||
const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[
|
|
||||||
// Add color to client (nullable for existing rows)
|
|
||||||
::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__client"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("color"),
|
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE)
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
// Remove old visit fields
|
|
||||||
::cot::db::migrations::Operation::remove_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("scheduled_at"),
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
))
|
|
||||||
.build(),
|
|
||||||
::cot::db::migrations::Operation::remove_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("duration_minutes"),
|
|
||||||
<Option<i32> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
).set_null(<Option<i32> as ::cot::db::DatabaseField>::NULLABLE))
|
|
||||||
.build(),
|
|
||||||
// Add new fields
|
|
||||||
::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("user_id"),
|
|
||||||
<cot::db::ForeignKey<crate::models::User> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.foreign_key(
|
|
||||||
<crate::models::User as ::cot::db::Model>::TABLE_NAME,
|
|
||||||
<crate::models::User as ::cot::db::Model>::PRIMARY_KEY_NAME,
|
|
||||||
::cot::db::ForeignKeyOnDeletePolicy::Restrict,
|
|
||||||
::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
|
|
||||||
)
|
|
||||||
.set_null(<cot::db::ForeignKey<crate::models::User> as ::cot::db::DatabaseField>::NULLABLE)
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("visit_date"),
|
|
||||||
<chrono::NaiveDate as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<chrono::NaiveDate as ::cot::db::DatabaseField>::NULLABLE)
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("time_start"),
|
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("time_end"),
|
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE)
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//! Migration: add client_feedback to Visit
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub(super) struct Migration;
|
|
||||||
impl ::cot::db::migrations::Migration for Migration {
|
|
||||||
const APP_NAME: &'static str = "web-petting";
|
|
||||||
const MIGRATION_NAME: &'static str = "m_0003_visit_feedback";
|
|
||||||
const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] =
|
|
||||||
&[::cot::db::migrations::MigrationDependency::migration(
|
|
||||||
"web-petting",
|
|
||||||
"m_0002_visit_schedule",
|
|
||||||
)];
|
|
||||||
const OPERATIONS: &'static [::cot::db::migrations::Operation] =
|
|
||||||
&[::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("client_feedback"),
|
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
)
|
|
||||||
.build()];
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//! Migration: add public_notes to Visit
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub(super) struct Migration;
|
|
||||||
impl ::cot::db::migrations::Migration for Migration {
|
|
||||||
const APP_NAME: &'static str = "web-petting";
|
|
||||||
const MIGRATION_NAME: &'static str = "m_0004_visit_public_notes";
|
|
||||||
const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] =
|
|
||||||
&[::cot::db::migrations::MigrationDependency::migration(
|
|
||||||
"web-petting",
|
|
||||||
"m_0003_visit_feedback",
|
|
||||||
)];
|
|
||||||
const OPERATIONS: &'static [::cot::db::migrations::Operation] =
|
|
||||||
&[::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__visit"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("public_notes"),
|
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
)
|
|
||||||
.build()];
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
//! Migration: create Testimonial table
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub(super) struct Migration;
|
|
||||||
impl ::cot::db::migrations::Migration for Migration {
|
|
||||||
const APP_NAME: &'static str = "web-petting";
|
|
||||||
const MIGRATION_NAME: &'static str = "m_0005_testimonials";
|
|
||||||
const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] =
|
|
||||||
&[::cot::db::migrations::MigrationDependency::migration(
|
|
||||||
"web-petting",
|
|
||||||
"m_0004_visit_public_notes",
|
|
||||||
)];
|
|
||||||
const OPERATIONS: &'static [::cot::db::migrations::Operation] =
|
|
||||||
&[::cot::db::migrations::Operation::create_model()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__testimonial"))
|
|
||||||
.fields(&[
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("id"),
|
|
||||||
<cot::db::Auto<i64> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.auto()
|
|
||||||
.primary_key()
|
|
||||||
.set_null(<cot::db::Auto<i64> as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("text"),
|
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("author_note"),
|
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("image_path"),
|
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("status"),
|
|
||||||
<String as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<String as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("sort_order"),
|
|
||||||
<i32 as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<i32 as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("created_at"),
|
|
||||||
<chrono::NaiveDateTime as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<chrono::NaiveDateTime as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
])
|
|
||||||
.build()];
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//! Migration: add telegram_chat_id and telegram_notifications to User
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub(super) struct Migration;
|
|
||||||
impl ::cot::db::migrations::Migration for Migration {
|
|
||||||
const APP_NAME: &'static str = "web-petting";
|
|
||||||
const MIGRATION_NAME: &'static str = "m_0006_user_telegram";
|
|
||||||
const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] =
|
|
||||||
&[::cot::db::migrations::MigrationDependency::migration(
|
|
||||||
"web-petting",
|
|
||||||
"m_0005_testimonials",
|
|
||||||
)];
|
|
||||||
const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[
|
|
||||||
::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__user"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("telegram_chat_id"),
|
|
||||||
<Option<String> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<Option<String> as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
::cot::db::migrations::Operation::add_field()
|
|
||||||
.table_name(::cot::db::Identifier::new("web_petting__user"))
|
|
||||||
.field(
|
|
||||||
::cot::db::migrations::Field::new(
|
|
||||||
::cot::db::Identifier::new("telegram_notifications"),
|
|
||||||
<Option<bool> as ::cot::db::DatabaseField>::TYPE,
|
|
||||||
)
|
|
||||||
.set_null(<Option<bool> as ::cot::db::DatabaseField>::NULLABLE),
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
+33
-18
@@ -276,6 +276,7 @@ async fn client_portal(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|m| {
|
.filter(|m| {
|
||||||
m.status == "active"
|
m.status == "active"
|
||||||
|
&& m.client_id.primary_key().unwrap() == client_id
|
||||||
&& m.visit_id
|
&& m.visit_id
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|fk| fk.primary_key().unwrap() == vid)
|
.map(|fk| fk.primary_key().unwrap() == vid)
|
||||||
@@ -343,22 +344,14 @@ async fn submit_feedback(
|
|||||||
serde_html_form::from_bytes(&bytes).map_err(|e| cot::Error::internal(e.to_string()))?;
|
serde_html_form::from_bytes(&bytes).map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
|
|
||||||
if !crate::turnstile::verify(&db, form.cf_turnstile_response.as_deref()).await? {
|
if !crate::turnstile::verify(&db, form.cf_turnstile_response.as_deref()).await? {
|
||||||
return Redirect::new(format!(
|
return Redirect::new(format!("/client/{}?lang={}", token_clone, lang.code()))
|
||||||
"/client/{}?lang={}",
|
.into_response();
|
||||||
token_clone,
|
|
||||||
lang.code()
|
|
||||||
))
|
|
||||||
.into_response();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
||||||
if visit.status == "deleted" {
|
if visit.status == "deleted" {
|
||||||
return Redirect::new(format!(
|
return Redirect::new(format!("/client/{}?lang={}", token_clone, lang.code()))
|
||||||
"/client/{}?lang={}",
|
.into_response();
|
||||||
token_clone,
|
|
||||||
lang.code()
|
|
||||||
))
|
|
||||||
.into_response();
|
|
||||||
}
|
}
|
||||||
if visit.client_id.primary_key().unwrap() == client_id {
|
if visit.client_id.primary_key().unwrap() == client_id {
|
||||||
visit.client_feedback = Some(form.feedback);
|
visit.client_feedback = Some(form.feedback);
|
||||||
@@ -401,7 +394,7 @@ async fn portal_media(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match tokio::fs::read(&media.file_path).await {
|
match crate::uploads::read_db_file(&media.file_path).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let content_type = match media.file_path.rsplit('.').next().unwrap_or("") {
|
let content_type = match media.file_path.rsplit('.').next().unwrap_or("") {
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
"jpg" | "jpeg" => "image/jpeg",
|
||||||
@@ -421,7 +414,17 @@ async fn portal_media(
|
|||||||
.insert("content-type", content_type.parse().unwrap());
|
.insert("content-type", content_type.parse().unwrap());
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
Err(_) => Html::new("404").into_response(),
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "uploads",
|
||||||
|
media_id,
|
||||||
|
db_path = %media.file_path,
|
||||||
|
resolved_path = %crate::uploads::resolved_display_path(&media.file_path),
|
||||||
|
error = %err,
|
||||||
|
"portal media file is missing or unreadable"
|
||||||
|
);
|
||||||
|
Html::new("404").into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +441,7 @@ async fn serve_testimonial_image(
|
|||||||
Some(p) => p.clone(),
|
Some(p) => p.clone(),
|
||||||
None => return Html::new("404").into_response(),
|
None => return Html::new("404").into_response(),
|
||||||
};
|
};
|
||||||
match tokio::fs::read(&path).await {
|
match crate::uploads::read_db_file(&path).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let content_type = match path.rsplit('.').next().unwrap_or("") {
|
let content_type = match path.rsplit('.').next().unwrap_or("") {
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
"jpg" | "jpeg" => "image/jpeg",
|
||||||
@@ -455,7 +458,17 @@ async fn serve_testimonial_image(
|
|||||||
.insert("cache-control", "public, max-age=86400".parse().unwrap());
|
.insert("cache-control", "public, max-age=86400".parse().unwrap());
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
Err(_) => Html::new("404").into_response(),
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "uploads",
|
||||||
|
testimonial_id = id,
|
||||||
|
db_path = %path,
|
||||||
|
resolved_path = %crate::uploads::resolved_display_path(&path),
|
||||||
|
error = %err,
|
||||||
|
"testimonial image is missing or unreadable"
|
||||||
|
);
|
||||||
|
Html::new("404").into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,8 +565,10 @@ async fn sitemap_xml(_request: Request, db: Database) -> cot::Result<Response> {
|
|||||||
domain = site_domain
|
domain = site_domain
|
||||||
);
|
);
|
||||||
let mut resp = Response::new(cot::Body::fixed(body.into_bytes()));
|
let mut resp = Response::new(cot::Body::fixed(body.into_bytes()));
|
||||||
resp.headers_mut()
|
resp.headers_mut().insert(
|
||||||
.insert("content-type", "application/xml; charset=utf-8".parse().unwrap());
|
"content-type",
|
||||||
|
"application/xml; charset=utf-8".parse().unwrap(),
|
||||||
|
);
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
const DEFAULT_UPLOAD_DIR: &str = "uploads";
|
||||||
|
const UPLOAD_DIR_ENV: &str = "WEB_PETTING_UPLOAD_DIR";
|
||||||
|
|
||||||
|
pub fn media_dir(client_id: i64, visit_id: i64) -> String {
|
||||||
|
format!("{DEFAULT_UPLOAD_DIR}/{client_id}/{visit_id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn testimonials_dir() -> String {
|
||||||
|
format!("{DEFAULT_UPLOAD_DIR}/testimonials")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn join_db_path(dir: &str, filename: &str) -> String {
|
||||||
|
format!("{}/{}", dir.trim_end_matches('/'), filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_db_path(db_path: &str) -> PathBuf {
|
||||||
|
let path = PathBuf::from(db_path);
|
||||||
|
if path.is_absolute() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(upload_root) = std::env::var_os(UPLOAD_DIR_ENV) else {
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
let upload_root = PathBuf::from(upload_root);
|
||||||
|
let logical_path = Path::new(db_path);
|
||||||
|
match logical_path.strip_prefix(DEFAULT_UPLOAD_DIR) {
|
||||||
|
Ok(stripped) => upload_root.join(stripped),
|
||||||
|
Err(_) => upload_root.join(logical_path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolved_display_path(db_path: &str) -> String {
|
||||||
|
resolve_db_path(db_path).display().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_logical_dir(db_dir: &str) -> std::io::Result<()> {
|
||||||
|
tokio::fs::create_dir_all(resolve_db_path(db_dir)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_db_file(db_path: &str, data: &[u8]) -> std::io::Result<()> {
|
||||||
|
let physical_path = resolve_db_path(db_path);
|
||||||
|
if let Some(parent) = physical_path.parent() {
|
||||||
|
tokio::fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
tokio::fs::write(physical_path, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_db_file(db_path: &str) -> std::io::Result<Vec<u8>> {
|
||||||
|
tokio::fs::read(resolve_db_path(db_path)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_db_file(db_path: &str) -> std::io::Result<()> {
|
||||||
|
tokio::fs::remove_file(resolve_db_path(db_path)).await
|
||||||
|
}
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
{% for item in &items %}
|
{% for item in &items %}
|
||||||
<div class="media-card">
|
<div class="media-card">
|
||||||
{% if item.media.file_type == "photo" %}
|
{% if item.media.file_type == "photo" %}
|
||||||
<a href="/admin/uploads/{{ item.media.id }}" data-lightbox="photo">
|
<a href="/admin/uploads/{{ item.media.id.unwrap() }}" data-lightbox="photo">
|
||||||
<img src="/admin/uploads/{{ item.media.id }}" alt="" loading="lazy">
|
<img src="/admin/uploads/{{ item.media.id.unwrap() }}" alt="" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/admin/uploads/{{ item.media.id }}" data-lightbox="video">
|
<a href="/admin/uploads/{{ item.media.id.unwrap() }}" data-lightbox="video">
|
||||||
<div class="video-thumb">🎬</div>
|
<div class="video-thumb">🎬</div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
{% if let Some(cap) = item.media.caption.as_deref() %}
|
{% if let Some(cap) = item.media.caption.as_deref() %}
|
||||||
<div style="font-size:0.82rem;color:#666;margin-top:0.2rem;">{{ cap }}</div>
|
<div style="font-size:0.82rem;color:#666;margin-top:0.2rem;">{{ cap }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/admin/media/{{ item.media.id }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');" style="margin-top:0.3rem;">
|
<form method="post" action="/admin/media/{{ item.media.id.unwrap() }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');" style="margin-top:0.3rem;">
|
||||||
<button class="button is-small is-danger is-outlined btn-sm">{{ t.media_delete }}</button>
|
<button class="button is-small is-danger is-outlined btn-sm">{{ t.media_delete }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -113,11 +113,11 @@
|
|||||||
{% for m in &media %}
|
{% for m in &media %}
|
||||||
<div class="visit-media-item">
|
<div class="visit-media-item">
|
||||||
{% if m.file_type == "photo" %}
|
{% if m.file_type == "photo" %}
|
||||||
<a href="/admin/uploads/{{ m.id }}" data-lightbox="photo">
|
<a href="/admin/uploads/{{ m.id.unwrap() }}" data-lightbox="photo">
|
||||||
<img src="/admin/uploads/{{ m.id }}" alt="" loading="lazy">
|
<img src="/admin/uploads/{{ m.id.unwrap() }}" alt="" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/admin/uploads/{{ m.id }}" data-lightbox="video">
|
<a href="/admin/uploads/{{ m.id.unwrap() }}" data-lightbox="video">
|
||||||
<div class="video-thumb-sm">🎬</div>
|
<div class="video-thumb-sm">🎬</div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
|
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% for m in &media %}
|
{% for m in &media %}
|
||||||
<form id="visit-media-delete-{{ m.id.unwrap() }}" method="post" action="/admin/media/{{ m.id }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');"></form>
|
<form id="visit-media-delete-{{ m.id.unwrap() }}" method="post" action="/admin/media/{{ m.id.unwrap() }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');"></form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<hr style="margin:1rem 0;">
|
<hr style="margin:1rem 0;">
|
||||||
|
|||||||
@@ -147,11 +147,11 @@
|
|||||||
<div class="media-row">
|
<div class="media-row">
|
||||||
{% for m in &pv.media %}
|
{% for m in &pv.media %}
|
||||||
{% if m.file_type == "photo" %}
|
{% if m.file_type == "photo" %}
|
||||||
<a href="/client/{{ client.media_token }}/media/{{ m.id }}" data-lightbox="photo">
|
<a href="/client/{{ client.media_token }}/media/{{ m.id.unwrap() }}" data-lightbox="photo">
|
||||||
<img src="/client/{{ client.media_token }}/media/{{ m.id }}" alt="" loading="lazy">
|
<img src="/client/{{ client.media_token }}/media/{{ m.id.unwrap() }}" alt="" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/client/{{ client.media_token }}/media/{{ m.id }}" data-lightbox="video">
|
<a href="/client/{{ client.media_token }}/media/{{ m.id.unwrap() }}" data-lightbox="video">
|
||||||
<div class="vid-thumb">🎬</div>
|
<div class="vid-thumb">🎬</div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user