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