use crate::field::{FieldDefinition, FieldType};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelDefinition {
pub name: String,
pub verbose_name: String,
pub verbose_name_singular: String,
pub table_name: String,
pub fields: Vec<FieldDefinition>,
pub primary_key: String,
pub list_display: Vec<String>,
pub search_fields: Vec<String>,
pub ordering: Vec<OrderingField>,
pub list_filter: Vec<String>,
pub readonly_fields: Vec<String>,
pub exclude: Vec<String>,
pub fieldsets: Vec<Fieldset>,
pub actions: Vec<AdminAction>,
pub icon: Option<String>,
pub list_template: Option<String>,
pub detail_template: Option<String>,
pub form_template: Option<String>,
pub inlines: Vec<InlineDefinition>,
pub can_add: bool,
pub can_edit: bool,
pub can_delete: bool,
pub can_export: bool,
}
impl ModelDefinition {
pub fn builder(name: impl Into<String>) -> ModelDefinitionBuilder {
ModelDefinitionBuilder::new(name)
}
pub fn get_field(&self, name: &str) -> Option<&FieldDefinition> {
self.fields.iter().find(|f| f.name == name)
}
pub fn display_fields(&self) -> Vec<&FieldDefinition> {
self.list_display
.iter()
.filter_map(|name| self.get_field(name))
.collect()
}
pub fn searchable_fields(&self) -> Vec<&FieldDefinition> {
self.search_fields
.iter()
.filter_map(|name| self.get_field(name))
.collect()
}
pub fn filterable_fields(&self) -> Vec<&FieldDefinition> {
self.list_filter
.iter()
.filter_map(|name| self.get_field(name))
.collect()
}
pub fn form_fields(&self) -> Vec<&FieldDefinition> {
self.fields
.iter()
.filter(|f| !f.primary_key && !self.exclude.contains(&f.name))
.collect()
}
pub fn pk_field(&self) -> Option<&FieldDefinition> {
self.get_field(&self.primary_key)
}
}
pub struct ModelDefinitionBuilder {
name: String,
verbose_name: Option<String>,
verbose_name_singular: Option<String>,
table_name: Option<String>,
fields: Vec<FieldDefinition>,
primary_key: String,
list_display: Vec<String>,
search_fields: Vec<String>,
ordering: Vec<OrderingField>,
list_filter: Vec<String>,
readonly_fields: Vec<String>,
exclude: Vec<String>,
fieldsets: Vec<Fieldset>,
actions: Vec<AdminAction>,
icon: Option<String>,
inlines: Vec<InlineDefinition>,
can_add: bool,
can_edit: bool,
can_delete: bool,
can_export: bool,
}
impl ModelDefinitionBuilder {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
Self {
name: name.clone(),
verbose_name: None,
verbose_name_singular: None,
table_name: None,
fields: Vec::new(),
primary_key: "id".to_string(),
list_display: Vec::new(),
search_fields: Vec::new(),
ordering: vec![OrderingField::desc("id")],
list_filter: Vec::new(),
readonly_fields: Vec::new(),
exclude: Vec::new(),
fieldsets: Vec::new(),
actions: Vec::new(),
icon: None,
inlines: Vec::new(),
can_add: true,
can_edit: true,
can_delete: true,
can_export: true,
}
}
pub fn verbose_name(mut self, name: impl Into<String>) -> Self {
self.verbose_name = Some(name.into());
self
}
pub fn table_name(mut self, name: impl Into<String>) -> Self {
self.table_name = Some(name.into());
self
}
pub fn field(mut self, field: FieldDefinition) -> Self {
if field.primary_key {
self.primary_key = field.name.clone();
}
self.fields.push(field);
self
}
pub fn id_field(self) -> Self {
self.field(
FieldDefinition::new("id", FieldType::BigInteger)
.primary_key()
.label("ID"),
)
}
pub fn timestamps(self) -> Self {
self.field(
FieldDefinition::new("created_at", FieldType::DateTime)
.readonly()
.label("Created"),
)
.field(
FieldDefinition::new("updated_at", FieldType::DateTime)
.readonly()
.label("Updated"),
)
}
pub fn list_display(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.list_display = fields.into_iter().map(Into::into).collect();
self
}
pub fn search_fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.search_fields = fields.into_iter().map(Into::into).collect();
self
}
pub fn ordering(mut self, fields: impl IntoIterator<Item = OrderingField>) -> Self {
self.ordering = fields.into_iter().collect();
self
}
pub fn list_filter(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.list_filter = fields.into_iter().map(Into::into).collect();
self
}
pub fn readonly_fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.readonly_fields = fields.into_iter().map(Into::into).collect();
self
}
pub fn exclude(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.exclude = fields.into_iter().map(Into::into).collect();
self
}
pub fn fieldset(mut self, fieldset: Fieldset) -> Self {
self.fieldsets.push(fieldset);
self
}
pub fn action(mut self, action: AdminAction) -> Self {
self.actions.push(action);
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn inline(mut self, inline: InlineDefinition) -> Self {
self.inlines.push(inline);
self
}
pub fn no_add(mut self) -> Self {
self.can_add = false;
self
}
pub fn no_edit(mut self) -> Self {
self.can_edit = false;
self
}
pub fn no_delete(mut self) -> Self {
self.can_delete = false;
self
}
pub fn build(self) -> ModelDefinition {
let verbose_name = self.verbose_name.unwrap_or_else(|| {
let name = self.name.replace('_', " ");
if name.ends_with('s') {
name
} else {
format!("{}s", name)
}
});
let verbose_name_singular = self.verbose_name_singular.unwrap_or_else(|| {
self.name.replace('_', " ")
});
let table_name = self.table_name.unwrap_or_else(|| {
self.name.to_lowercase().replace(' ', "_")
});
let list_display = if self.list_display.is_empty() {
self.fields.iter().take(5).map(|f| f.name.clone()).collect()
} else {
self.list_display
};
ModelDefinition {
name: self.name,
verbose_name,
verbose_name_singular,
table_name,
fields: self.fields,
primary_key: self.primary_key,
list_display,
search_fields: self.search_fields,
ordering: self.ordering,
list_filter: self.list_filter,
readonly_fields: self.readonly_fields,
exclude: self.exclude,
fieldsets: self.fieldsets,
actions: self.actions,
icon: self.icon,
list_template: None,
detail_template: None,
form_template: None,
inlines: self.inlines,
can_add: self.can_add,
can_edit: self.can_edit,
can_delete: self.can_delete,
can_export: self.can_export,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderingField {
pub field: String,
pub descending: bool,
}
impl OrderingField {
pub fn asc(field: impl Into<String>) -> Self {
Self {
field: field.into(),
descending: false,
}
}
pub fn desc(field: impl Into<String>) -> Self {
Self {
field: field.into(),
descending: true,
}
}
pub fn as_sql(&self) -> String {
format!(
"{} {}",
self.field,
if self.descending { "DESC" } else { "ASC" }
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fieldset {
pub name: Option<String>,
pub fields: Vec<String>,
pub classes: Vec<String>,
pub description: Option<String>,
pub collapsible: bool,
pub collapsed: bool,
}
impl Fieldset {
pub fn new(fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
name: None,
fields: fields.into_iter().map(Into::into).collect(),
classes: Vec::new(),
description: None,
collapsible: false,
collapsed: false,
}
}
pub fn named(name: impl Into<String>, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
name: Some(name.into()),
fields: fields.into_iter().map(Into::into).collect(),
classes: Vec::new(),
description: None,
collapsible: false,
collapsed: false,
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn collapsible(mut self) -> Self {
self.collapsible = true;
self
}
pub fn collapsed(mut self) -> Self {
self.collapsible = true;
self.collapsed = true;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminAction {
pub name: String,
pub label: String,
pub description: Option<String>,
pub icon: Option<String>,
pub dangerous: bool,
pub requires_selection: bool,
}
impl AdminAction {
pub fn new(name: impl Into<String>, label: impl Into<String>) -> Self {
Self {
name: name.into(),
label: label.into(),
description: None,
icon: None,
dangerous: false,
requires_selection: true,
}
}
pub fn delete() -> Self {
Self {
name: "delete".to_string(),
label: "Delete selected".to_string(),
description: Some("Permanently delete selected items".to_string()),
icon: Some("trash".to_string()),
dangerous: true,
requires_selection: true,
}
}
pub fn export() -> Self {
Self {
name: "export".to_string(),
label: "Export".to_string(),
description: Some("Export selected items to CSV".to_string()),
icon: Some("download".to_string()),
dangerous: false,
requires_selection: false,
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn dangerous(mut self) -> Self {
self.dangerous = true;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InlineDefinition {
pub model: String,
pub fk_field: String,
pub fields: Vec<String>,
pub extra: usize,
pub max_num: Option<usize>,
pub min_num: usize,
pub can_delete: bool,
pub verbose_name: Option<String>,
}
impl InlineDefinition {
pub fn new(model: impl Into<String>, fk_field: impl Into<String>) -> Self {
Self {
model: model.into(),
fk_field: fk_field.into(),
fields: Vec::new(),
extra: 3,
max_num: None,
min_num: 0,
can_delete: true,
verbose_name: None,
}
}
pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.fields = fields.into_iter().map(Into::into).collect();
self
}
pub fn extra(mut self, extra: usize) -> Self {
self.extra = extra;
self
}
pub fn max_num(mut self, max: usize) -> Self {
self.max_num = Some(max);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_builder() {
let model = ModelDefinition::builder("user")
.id_field()
.field(FieldDefinition::new("name", FieldType::String).required())
.field(FieldDefinition::new("email", FieldType::Email))
.timestamps()
.list_display(["id", "name", "email"])
.search_fields(["name", "email"])
.build();
assert_eq!(model.name, "user");
assert_eq!(model.verbose_name, "users");
assert_eq!(model.primary_key, "id");
assert_eq!(model.fields.len(), 5);
}
#[test]
fn test_fieldset() {
let fieldset = Fieldset::named("Personal Info", ["name", "email"])
.description("User's personal information")
.collapsible();
assert_eq!(fieldset.name, Some("Personal Info".to_string()));
assert_eq!(fieldset.fields.len(), 2);
assert!(fieldset.collapsible);
}
#[test]
fn test_ordering() {
let desc = OrderingField::desc("created_at");
assert_eq!(desc.as_sql(), "created_at DESC");
let asc = OrderingField::asc("name");
assert_eq!(asc.as_sql(), "name ASC");
}
}