use async_trait::async_trait;
use reinhardt_core::exception::{Error, Result};
use reinhardt_core::security::xss::escape_html;
use reinhardt_db::orm::Model;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::marker::PhantomData;
#[derive(Debug, thiserror::Error)]
pub enum AdminError {
#[error("Model not found: {0}")]
ModelNotFound(String),
#[error("Field not found: {0}")]
FieldNotFound(String),
#[error("Invalid filter: {0}")]
InvalidFilter(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Query error: {0}")]
QueryError(String),
}
impl From<AdminError> for Error {
fn from(err: AdminError) -> Self {
Error::Validation(err.to_string())
}
}
#[async_trait]
pub trait AdminView: Send + Sync {
async fn render(&self) -> Result<String>;
fn has_view_permission(&self) -> bool {
true
}
fn has_add_permission(&self) -> bool {
true
}
fn has_change_permission(&self) -> bool {
true
}
fn has_delete_permission(&self) -> bool {
true
}
}
pub struct ModelAdmin<M>
where
M: Model + Serialize + for<'de> Deserialize<'de> + Send + Sync + Clone,
{
list_display: Vec<String>,
search_fields: Vec<String>,
list_filter: Vec<String>,
ordering: Vec<String>,
list_per_page: usize,
show_full_result_count: bool,
readonly_fields: Vec<String>,
queryset: Option<Vec<M>>,
_phantom: PhantomData<M>,
}
impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone> ModelAdmin<T> {
pub fn new() -> Self {
Self {
list_display: vec![],
list_filter: vec![],
search_fields: vec![],
ordering: vec![],
list_per_page: 100,
show_full_result_count: true,
readonly_fields: vec![],
queryset: None,
_phantom: PhantomData,
}
}
pub fn with_list_display(mut self, fields: Vec<String>) -> Self {
self.list_display = fields;
self
}
pub fn with_list_filter(mut self, fields: Vec<String>) -> Self {
self.list_filter = fields;
self
}
pub fn with_search_fields(mut self, fields: Vec<String>) -> Self {
self.search_fields = fields;
self
}
pub fn with_ordering(mut self, fields: Vec<String>) -> Self {
self.ordering = fields;
self
}
pub fn with_list_per_page(mut self, count: usize) -> Self {
self.list_per_page = count;
self
}
pub fn with_show_full_result_count(mut self, show: bool) -> Self {
self.show_full_result_count = show;
self
}
pub fn with_readonly_fields(mut self, fields: Vec<String>) -> Self {
self.readonly_fields = fields;
self
}
pub fn with_queryset(mut self, queryset: Vec<T>) -> Self {
self.queryset = Some(queryset);
self
}
pub fn list_display(&self) -> &[String] {
&self.list_display
}
pub fn list_filter(&self) -> &[String] {
&self.list_filter
}
pub fn search_fields(&self) -> &[String] {
&self.search_fields
}
pub fn ordering(&self) -> &[String] {
&self.ordering
}
pub fn list_per_page(&self) -> usize {
self.list_per_page
}
pub fn show_full_result_count(&self) -> bool {
self.show_full_result_count
}
pub fn readonly_fields(&self) -> &[String] {
&self.readonly_fields
}
pub async fn get_queryset(&self) -> Result<Vec<T>> {
match &self.queryset {
Some(qs) => Ok(qs.clone()),
None => Ok(Vec::new()),
}
}
pub async fn render_list(&self) -> Result<String> {
let objects = self.get_queryset().await?;
let count = objects.len();
let mut html = String::from("<div class=\"admin-list\">\n");
html.push_str(&format!("<h2>{} List</h2>\n", escape_html(T::table_name())));
html.push_str(&format!("<p>Total: {} items</p>\n", count));
html.push_str("<table>\n<thead>\n<tr>\n");
for field in &self.list_display {
html.push_str(&format!("<th>{}</th>\n", escape_html(field)));
}
html.push_str("</tr>\n</thead>\n<tbody>\n");
for obj in objects {
html.push_str("<tr>\n");
let obj_json =
serde_json::to_value(&obj).map_err(|e| Error::Serialization(e.to_string()))?;
for field in &self.list_display {
let value = obj_json
.get(field)
.map(|v| v.to_string())
.unwrap_or_else(|| "-".to_string());
html.push_str(&format!("<td>{}</td>\n", escape_html(&value)));
}
html.push_str("</tr>\n");
}
html.push_str("</tbody>\n</table>\n</div>");
Ok(html)
}
pub fn search(&self, query: &str, objects: Vec<T>) -> Vec<T> {
if query.is_empty() || self.search_fields.is_empty() {
return objects;
}
objects
.into_iter()
.filter(|obj| {
let obj_json = serde_json::to_value(obj).ok();
if let Some(json) = obj_json {
self.search_fields.iter().any(|field| {
json.get(field)
.and_then(|v| v.as_str())
.map(|s| s.to_lowercase().contains(&query.to_lowercase()))
.unwrap_or(false)
})
} else {
false
}
})
.collect()
}
pub fn filter(&self, filters: &HashMap<String, String>, objects: Vec<T>) -> Vec<T> {
if filters.is_empty() {
return objects;
}
objects
.into_iter()
.filter(|obj| {
let obj_json = serde_json::to_value(obj).ok();
if let Some(json) = obj_json {
filters.iter().all(|(field, value)| {
json.get(field)
.map(|v| {
match v {
serde_json::Value::String(s) => s == value,
serde_json::Value::Bool(b) => {
value.to_lowercase() == b.to_string()
}
serde_json::Value::Number(n) => {
let n_str = n.to_string();
n_str == value.as_str()
}
_ => {
if let Some(s) = v.as_str() {
s == value.as_str()
} else {
let v_str = v.to_string();
v_str == value.as_str()
}
}
}
})
.unwrap_or(false)
})
} else {
false
}
})
.collect()
}
}
#[async_trait]
impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync> AdminView
for ModelAdmin<T>
{
async fn render(&self) -> Result<String> {
self.render_list().await
}
}
impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone> Default for ModelAdmin<T> {
fn default() -> Self {
Self::new()
}
}