Skip to main content

reinhardt_views/
admin.rs

1//! Django Admin Framework
2//!
3//! Auto-generated CRUD interface for models with:
4//! - ModelAdmin configuration
5//! - AdminSite registration
6//! - List/Change/Add views
7//! - Filters and search
8//! - Bulk actions
9//! - Permissions integration
10
11use async_trait::async_trait;
12use reinhardt_core::exception::{Error, Result};
13use reinhardt_core::security::xss::escape_html;
14use reinhardt_db::orm::Model;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::marker::PhantomData;
18
19/// Error type for admin operations
20#[derive(Debug, thiserror::Error)]
21pub enum AdminError {
22	/// The requested model was not found.
23	#[error("Model not found: {0}")]
24	ModelNotFound(String),
25
26	/// The requested field was not found on the model.
27	#[error("Field not found: {0}")]
28	FieldNotFound(String),
29
30	/// The provided filter expression is invalid.
31	#[error("Invalid filter: {0}")]
32	InvalidFilter(String),
33
34	/// The current user does not have permission for this action.
35	#[error("Permission denied: {0}")]
36	PermissionDenied(String),
37
38	/// A database query error occurred.
39	#[error("Query error: {0}")]
40	QueryError(String),
41}
42
43impl From<AdminError> for Error {
44	fn from(err: AdminError) -> Self {
45		Error::Validation(err.to_string())
46	}
47}
48
49/// Base trait for admin views
50#[async_trait]
51pub trait AdminView: Send + Sync {
52	/// Render the admin view
53	async fn render(&self) -> Result<String>;
54
55	/// Check if the current user has permission to view this admin
56	fn has_view_permission(&self) -> bool {
57		true
58	}
59
60	/// Check if the current user has permission to add objects
61	fn has_add_permission(&self) -> bool {
62		true
63	}
64
65	/// Check if the current user has permission to change objects
66	fn has_change_permission(&self) -> bool {
67		true
68	}
69
70	/// Check if the current user has permission to delete objects
71	fn has_delete_permission(&self) -> bool {
72		true
73	}
74}
75
76/// A registry for admin interfaces similar to Django's ModelAdmin.
77///
78/// This allows you to register models and customize how they appear
79/// in admin interfaces.
80///
81/// # Examples
82///
83/// ```rust,no_run
84/// use reinhardt_views::admin::ModelAdmin;
85/// use reinhardt_db::orm::Model;
86/// use serde::{Serialize, Deserialize};
87///
88/// #[derive(Debug, Clone, Serialize, Deserialize)]
89/// struct Article {
90///     id: Option<i64>,
91///     title: String,
92///     content: String,
93/// }
94///
95/// #[derive(Clone)]
96/// struct ArticleFields;
97///
98/// impl reinhardt_db::orm::FieldSelector for ArticleFields {
99///     fn with_alias(self, _alias: &str) -> Self {
100///         self
101///     }
102/// }
103///
104/// impl Model for Article {
105///     type PrimaryKey = i64;
106///     type Fields = ArticleFields;
107///     type Objects = reinhardt_db::orm::Manager<Self>;
108///     fn table_name() -> &'static str { "articles" }
109///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
110///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
111///     fn new_fields() -> Self::Fields { ArticleFields }
112/// }
113///
114/// let admin = ModelAdmin::<Article>::new()
115///     .with_list_display(vec!["id".to_string(), "title".to_string()])
116///     .with_search_fields(vec!["title".to_string(), "content".to_string()]);
117/// ```
118pub struct ModelAdmin<M>
119where
120	M: Model + Serialize + for<'de> Deserialize<'de> + Send + Sync + Clone,
121{
122	list_display: Vec<String>,
123	search_fields: Vec<String>,
124	list_filter: Vec<String>,
125	ordering: Vec<String>,
126	list_per_page: usize,
127	show_full_result_count: bool,
128	readonly_fields: Vec<String>,
129	queryset: Option<Vec<M>>,
130	_phantom: PhantomData<M>,
131}
132
133impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone> ModelAdmin<T> {
134	/// Creates a new ModelAdmin with default settings
135	///
136	/// # Examples
137	///
138	/// ```
139	/// use reinhardt_views::admin::ModelAdmin;
140	/// use reinhardt_db::orm::Model;
141	/// use serde::{Serialize, Deserialize};
142	///
143	/// #[derive(Debug, Clone, Serialize, Deserialize)]
144	/// struct User {
145	///     id: Option<i64>,
146	///     username: String,
147	/// }
148	///
149	/// #[derive(Clone)]
150	/// struct UserFields;
151	///
152	/// impl reinhardt_db::orm::FieldSelector for UserFields {
153	///     fn with_alias(self, _alias: &str) -> Self {
154	///         self
155	///     }
156	/// }
157	///
158	/// impl Model for User {
159	///     type PrimaryKey = i64;
160	///     type Fields = UserFields;
161	///     type Objects = reinhardt_db::orm::Manager<Self>;
162	///     fn table_name() -> &'static str { "users" }
163	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
164	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
165	///     fn new_fields() -> Self::Fields { UserFields }
166	/// }
167	///
168	/// let admin = ModelAdmin::<User>::new();
169	/// assert_eq!(admin.list_per_page(), 100);
170	/// ```
171	pub fn new() -> Self {
172		Self {
173			list_display: vec![],
174			list_filter: vec![],
175			search_fields: vec![],
176			ordering: vec![],
177			list_per_page: 100,
178			show_full_result_count: true,
179			readonly_fields: vec![],
180			queryset: None,
181			_phantom: PhantomData,
182		}
183	}
184
185	/// Sets the fields to display in the list view
186	///
187	/// # Examples
188	///
189	/// ```
190	/// use reinhardt_views::admin::ModelAdmin;
191	/// use reinhardt_db::orm::Model;
192	/// use serde::{Serialize, Deserialize};
193	///
194	/// #[derive(Debug, Clone, Serialize, Deserialize)]
195	/// struct Article {
196	///     id: Option<i64>,
197	///     title: String,
198	/// }
199	///
200	/// #[derive(Clone)]
201	/// struct ArticleFields;
202	///
203	/// impl reinhardt_db::orm::FieldSelector for ArticleFields {
204	///     fn with_alias(self, _alias: &str) -> Self {
205	///         self
206	///     }
207	/// }
208	///
209	/// impl Model for Article {
210	///     type PrimaryKey = i64;
211	///     type Fields = ArticleFields;
212	///     type Objects = reinhardt_db::orm::Manager<Self>;
213	///     fn table_name() -> &'static str { "articles" }
214	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
215	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
216	///     fn new_fields() -> Self::Fields { ArticleFields }
217	/// }
218	///
219	/// let admin = ModelAdmin::<Article>::new()
220	///     .with_list_display(vec!["id".to_string(), "title".to_string()]);
221	/// assert_eq!(admin.list_display().len(), 2);
222	/// ```
223	pub fn with_list_display(mut self, fields: Vec<String>) -> Self {
224		self.list_display = fields;
225		self
226	}
227
228	/// Sets the fields to filter by in the list view
229	pub fn with_list_filter(mut self, fields: Vec<String>) -> Self {
230		self.list_filter = fields;
231		self
232	}
233
234	/// Sets the fields to search in
235	pub fn with_search_fields(mut self, fields: Vec<String>) -> Self {
236		self.search_fields = fields;
237		self
238	}
239
240	/// Sets the ordering for the list view
241	pub fn with_ordering(mut self, fields: Vec<String>) -> Self {
242		self.ordering = fields;
243		self
244	}
245
246	/// Sets the number of items per page
247	pub fn with_list_per_page(mut self, count: usize) -> Self {
248		self.list_per_page = count;
249		self
250	}
251
252	/// Sets whether to show full result count
253	pub fn with_show_full_result_count(mut self, show: bool) -> Self {
254		self.show_full_result_count = show;
255		self
256	}
257
258	/// Sets the readonly fields
259	pub fn with_readonly_fields(mut self, fields: Vec<String>) -> Self {
260		self.readonly_fields = fields;
261		self
262	}
263
264	/// Sets a custom queryset for the admin
265	pub fn with_queryset(mut self, queryset: Vec<T>) -> Self {
266		self.queryset = Some(queryset);
267		self
268	}
269
270	/// Gets the list of fields to display
271	pub fn list_display(&self) -> &[String] {
272		&self.list_display
273	}
274
275	/// Gets the list of filter fields
276	pub fn list_filter(&self) -> &[String] {
277		&self.list_filter
278	}
279
280	/// Gets the search fields
281	pub fn search_fields(&self) -> &[String] {
282		&self.search_fields
283	}
284
285	/// Gets the ordering fields
286	pub fn ordering(&self) -> &[String] {
287		&self.ordering
288	}
289
290	/// Gets the number of items per page
291	pub fn list_per_page(&self) -> usize {
292		self.list_per_page
293	}
294
295	/// Gets whether to show full result count
296	pub fn show_full_result_count(&self) -> bool {
297		self.show_full_result_count
298	}
299
300	/// Gets the readonly fields
301	pub fn readonly_fields(&self) -> &[String] {
302		&self.readonly_fields
303	}
304
305	/// Gets the queryset for this admin
306	pub async fn get_queryset(&self) -> Result<Vec<T>> {
307		match &self.queryset {
308			Some(qs) => Ok(qs.clone()),
309			None => Ok(Vec::new()),
310		}
311	}
312
313	/// Renders the list view as HTML
314	pub async fn render_list(&self) -> Result<String> {
315		let objects = self.get_queryset().await?;
316		let count = objects.len();
317
318		let mut html = String::from("<div class=\"admin-list\">\n");
319		html.push_str(&format!("<h2>{} List</h2>\n", escape_html(T::table_name())));
320		html.push_str(&format!("<p>Total: {} items</p>\n", count));
321
322		// Table header
323		html.push_str("<table>\n<thead>\n<tr>\n");
324		for field in &self.list_display {
325			html.push_str(&format!("<th>{}</th>\n", escape_html(field)));
326		}
327		html.push_str("</tr>\n</thead>\n<tbody>\n");
328
329		// Table rows
330		for obj in objects {
331			html.push_str("<tr>\n");
332			let obj_json =
333				serde_json::to_value(&obj).map_err(|e| Error::Serialization(e.to_string()))?;
334
335			for field in &self.list_display {
336				let value = obj_json
337					.get(field)
338					.map(|v| v.to_string())
339					.unwrap_or_else(|| "-".to_string());
340				// Escape user-controlled values to prevent XSS
341				html.push_str(&format!("<td>{}</td>\n", escape_html(&value)));
342			}
343			html.push_str("</tr>\n");
344		}
345
346		html.push_str("</tbody>\n</table>\n</div>");
347
348		Ok(html)
349	}
350
351	/// Searches the queryset based on search fields
352	pub fn search(&self, query: &str, objects: Vec<T>) -> Vec<T> {
353		if query.is_empty() || self.search_fields.is_empty() {
354			return objects;
355		}
356
357		objects
358			.into_iter()
359			.filter(|obj| {
360				let obj_json = serde_json::to_value(obj).ok();
361				if let Some(json) = obj_json {
362					self.search_fields.iter().any(|field| {
363						json.get(field)
364							.and_then(|v| v.as_str())
365							.map(|s| s.to_lowercase().contains(&query.to_lowercase()))
366							.unwrap_or(false)
367					})
368				} else {
369					false
370				}
371			})
372			.collect()
373	}
374
375	/// Filters the queryset based on filter criteria
376	pub fn filter(&self, filters: &HashMap<String, String>, objects: Vec<T>) -> Vec<T> {
377		if filters.is_empty() {
378			return objects;
379		}
380
381		objects
382			.into_iter()
383			.filter(|obj| {
384				let obj_json = serde_json::to_value(obj).ok();
385				if let Some(json) = obj_json {
386					filters.iter().all(|(field, value)| {
387						json.get(field)
388							.map(|v| {
389								// Handle different value types
390								match v {
391									serde_json::Value::String(s) => s == value,
392									serde_json::Value::Bool(b) => {
393										value.to_lowercase() == b.to_string()
394									}
395									serde_json::Value::Number(n) => {
396										// Create owned string once and compare
397										let n_str = n.to_string();
398										n_str == value.as_str()
399									}
400									_ => {
401										// For other types, compare string representations
402										if let Some(s) = v.as_str() {
403											s == value.as_str()
404										} else {
405											// Create owned string once and compare with borrowed value
406											let v_str = v.to_string();
407											v_str == value.as_str()
408										}
409									}
410								}
411							})
412							.unwrap_or(false)
413					})
414				} else {
415					false
416				}
417			})
418			.collect()
419	}
420}
421
422#[async_trait]
423impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync> AdminView
424	for ModelAdmin<T>
425{
426	async fn render(&self) -> Result<String> {
427		self.render_list().await
428	}
429}
430
431impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone> Default for ModelAdmin<T> {
432	fn default() -> Self {
433		Self::new()
434	}
435}