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///     fn table_name() -> &'static str { "articles" }
108///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
109///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
110///     fn new_fields() -> Self::Fields { ArticleFields }
111/// }
112///
113/// let admin = ModelAdmin::<Article>::new()
114///     .with_list_display(vec!["id".to_string(), "title".to_string()])
115///     .with_search_fields(vec!["title".to_string(), "content".to_string()]);
116/// ```
117pub struct ModelAdmin<M>
118where
119	M: Model + Serialize + for<'de> Deserialize<'de> + Send + Sync + Clone,
120{
121	list_display: Vec<String>,
122	search_fields: Vec<String>,
123	list_filter: Vec<String>,
124	ordering: Vec<String>,
125	list_per_page: usize,
126	show_full_result_count: bool,
127	readonly_fields: Vec<String>,
128	queryset: Option<Vec<M>>,
129	_phantom: PhantomData<M>,
130}
131
132impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone> ModelAdmin<T> {
133	/// Creates a new ModelAdmin with default settings
134	///
135	/// # Examples
136	///
137	/// ```
138	/// use reinhardt_views::admin::ModelAdmin;
139	/// use reinhardt_db::orm::Model;
140	/// use serde::{Serialize, Deserialize};
141	///
142	/// #[derive(Debug, Clone, Serialize, Deserialize)]
143	/// struct User {
144	///     id: Option<i64>,
145	///     username: String,
146	/// }
147	///
148	/// #[derive(Clone)]
149	/// struct UserFields;
150	///
151	/// impl reinhardt_db::orm::FieldSelector for UserFields {
152	///     fn with_alias(self, _alias: &str) -> Self {
153	///         self
154	///     }
155	/// }
156	///
157	/// impl Model for User {
158	///     type PrimaryKey = i64;
159	///     type Fields = UserFields;
160	///     fn table_name() -> &'static str { "users" }
161	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
162	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
163	///     fn new_fields() -> Self::Fields { UserFields }
164	/// }
165	///
166	/// let admin = ModelAdmin::<User>::new();
167	/// assert_eq!(admin.list_per_page(), 100);
168	/// ```
169	pub fn new() -> Self {
170		Self {
171			list_display: vec![],
172			list_filter: vec![],
173			search_fields: vec![],
174			ordering: vec![],
175			list_per_page: 100,
176			show_full_result_count: true,
177			readonly_fields: vec![],
178			queryset: None,
179			_phantom: PhantomData,
180		}
181	}
182
183	/// Sets the fields to display in the list view
184	///
185	/// # Examples
186	///
187	/// ```
188	/// use reinhardt_views::admin::ModelAdmin;
189	/// use reinhardt_db::orm::Model;
190	/// use serde::{Serialize, Deserialize};
191	///
192	/// #[derive(Debug, Clone, Serialize, Deserialize)]
193	/// struct Article {
194	///     id: Option<i64>,
195	///     title: String,
196	/// }
197	///
198	/// #[derive(Clone)]
199	/// struct ArticleFields;
200	///
201	/// impl reinhardt_db::orm::FieldSelector for ArticleFields {
202	///     fn with_alias(self, _alias: &str) -> Self {
203	///         self
204	///     }
205	/// }
206	///
207	/// impl Model for Article {
208	///     type PrimaryKey = i64;
209	///     type Fields = ArticleFields;
210	///     fn table_name() -> &'static str { "articles" }
211	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
212	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
213	///     fn new_fields() -> Self::Fields { ArticleFields }
214	/// }
215	///
216	/// let admin = ModelAdmin::<Article>::new()
217	///     .with_list_display(vec!["id".to_string(), "title".to_string()]);
218	/// assert_eq!(admin.list_display().len(), 2);
219	/// ```
220	pub fn with_list_display(mut self, fields: Vec<String>) -> Self {
221		self.list_display = fields;
222		self
223	}
224
225	/// Sets the fields to filter by in the list view
226	pub fn with_list_filter(mut self, fields: Vec<String>) -> Self {
227		self.list_filter = fields;
228		self
229	}
230
231	/// Sets the fields to search in
232	pub fn with_search_fields(mut self, fields: Vec<String>) -> Self {
233		self.search_fields = fields;
234		self
235	}
236
237	/// Sets the ordering for the list view
238	pub fn with_ordering(mut self, fields: Vec<String>) -> Self {
239		self.ordering = fields;
240		self
241	}
242
243	/// Sets the number of items per page
244	pub fn with_list_per_page(mut self, count: usize) -> Self {
245		self.list_per_page = count;
246		self
247	}
248
249	/// Sets whether to show full result count
250	pub fn with_show_full_result_count(mut self, show: bool) -> Self {
251		self.show_full_result_count = show;
252		self
253	}
254
255	/// Sets the readonly fields
256	pub fn with_readonly_fields(mut self, fields: Vec<String>) -> Self {
257		self.readonly_fields = fields;
258		self
259	}
260
261	/// Sets a custom queryset for the admin
262	pub fn with_queryset(mut self, queryset: Vec<T>) -> Self {
263		self.queryset = Some(queryset);
264		self
265	}
266
267	/// Gets the list of fields to display
268	pub fn list_display(&self) -> &[String] {
269		&self.list_display
270	}
271
272	/// Gets the list of filter fields
273	pub fn list_filter(&self) -> &[String] {
274		&self.list_filter
275	}
276
277	/// Gets the search fields
278	pub fn search_fields(&self) -> &[String] {
279		&self.search_fields
280	}
281
282	/// Gets the ordering fields
283	pub fn ordering(&self) -> &[String] {
284		&self.ordering
285	}
286
287	/// Gets the number of items per page
288	pub fn list_per_page(&self) -> usize {
289		self.list_per_page
290	}
291
292	/// Gets whether to show full result count
293	pub fn show_full_result_count(&self) -> bool {
294		self.show_full_result_count
295	}
296
297	/// Gets the readonly fields
298	pub fn readonly_fields(&self) -> &[String] {
299		&self.readonly_fields
300	}
301
302	/// Gets the queryset for this admin
303	pub async fn get_queryset(&self) -> Result<Vec<T>> {
304		match &self.queryset {
305			Some(qs) => Ok(qs.clone()),
306			None => Ok(Vec::new()),
307		}
308	}
309
310	/// Renders the list view as HTML
311	pub async fn render_list(&self) -> Result<String> {
312		let objects = self.get_queryset().await?;
313		let count = objects.len();
314
315		let mut html = String::from("<div class=\"admin-list\">\n");
316		html.push_str(&format!("<h2>{} List</h2>\n", escape_html(T::table_name())));
317		html.push_str(&format!("<p>Total: {} items</p>\n", count));
318
319		// Table header
320		html.push_str("<table>\n<thead>\n<tr>\n");
321		for field in &self.list_display {
322			html.push_str(&format!("<th>{}</th>\n", escape_html(field)));
323		}
324		html.push_str("</tr>\n</thead>\n<tbody>\n");
325
326		// Table rows
327		for obj in objects {
328			html.push_str("<tr>\n");
329			let obj_json =
330				serde_json::to_value(&obj).map_err(|e| Error::Serialization(e.to_string()))?;
331
332			for field in &self.list_display {
333				let value = obj_json
334					.get(field)
335					.map(|v| v.to_string())
336					.unwrap_or_else(|| "-".to_string());
337				// Escape user-controlled values to prevent XSS
338				html.push_str(&format!("<td>{}</td>\n", escape_html(&value)));
339			}
340			html.push_str("</tr>\n");
341		}
342
343		html.push_str("</tbody>\n</table>\n</div>");
344
345		Ok(html)
346	}
347
348	/// Searches the queryset based on search fields
349	pub fn search(&self, query: &str, objects: Vec<T>) -> Vec<T> {
350		if query.is_empty() || self.search_fields.is_empty() {
351			return objects;
352		}
353
354		objects
355			.into_iter()
356			.filter(|obj| {
357				let obj_json = serde_json::to_value(obj).ok();
358				if let Some(json) = obj_json {
359					self.search_fields.iter().any(|field| {
360						json.get(field)
361							.and_then(|v| v.as_str())
362							.map(|s| s.to_lowercase().contains(&query.to_lowercase()))
363							.unwrap_or(false)
364					})
365				} else {
366					false
367				}
368			})
369			.collect()
370	}
371
372	/// Filters the queryset based on filter criteria
373	pub fn filter(&self, filters: &HashMap<String, String>, objects: Vec<T>) -> Vec<T> {
374		if filters.is_empty() {
375			return objects;
376		}
377
378		objects
379			.into_iter()
380			.filter(|obj| {
381				let obj_json = serde_json::to_value(obj).ok();
382				if let Some(json) = obj_json {
383					filters.iter().all(|(field, value)| {
384						json.get(field)
385							.map(|v| {
386								// Handle different value types
387								match v {
388									serde_json::Value::String(s) => s == value,
389									serde_json::Value::Bool(b) => {
390										value.to_lowercase() == b.to_string()
391									}
392									serde_json::Value::Number(n) => {
393										// Create owned string once and compare
394										let n_str = n.to_string();
395										n_str == value.as_str()
396									}
397									_ => {
398										// For other types, compare string representations
399										if let Some(s) = v.as_str() {
400											s == value.as_str()
401										} else {
402											// Create owned string once and compare with borrowed value
403											let v_str = v.to_string();
404											v_str == value.as_str()
405										}
406									}
407								}
408							})
409							.unwrap_or(false)
410					})
411				} else {
412					false
413				}
414			})
415			.collect()
416	}
417}
418
419#[async_trait]
420impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync> AdminView
421	for ModelAdmin<T>
422{
423	async fn render(&self) -> Result<String> {
424		self.render_list().await
425	}
426}
427
428impl<T: Model + Serialize + for<'de> Deserialize<'de> + Clone> Default for ModelAdmin<T> {
429	fn default() -> Self {
430		Self::new()
431	}
432}