Skip to main content

reinhardt_admin/core/
model_admin.rs

1//! Model admin configuration and trait
2//!
3//! This module defines how models are displayed and managed in the admin interface.
4
5use async_trait::async_trait;
6
7/// Trait for configuring model administration
8///
9/// Implement this trait to customize how a model is displayed and edited in the admin.
10#[async_trait]
11pub trait ModelAdmin: Send + Sync {
12	/// Get the model name
13	fn model_name(&self) -> &str;
14
15	/// Get the database table name
16	///
17	/// By default, returns the model name in lowercase.
18	fn table_name(&self) -> &str {
19		// Default implementation returns empty string
20		// Override in implementations to return actual table name
21		""
22	}
23
24	/// Get the primary key field name
25	///
26	/// By default, returns "id".
27	fn pk_field(&self) -> &str {
28		"id"
29	}
30
31	/// Fields to display in list view
32	fn list_display(&self) -> Vec<&str> {
33		vec!["id"]
34	}
35
36	/// Fields that can be used for filtering
37	fn list_filter(&self) -> Vec<&str> {
38		vec![]
39	}
40
41	/// Fields that can be searched
42	fn search_fields(&self) -> Vec<&str> {
43		vec![]
44	}
45
46	/// Fields to display in forms (None = all fields)
47	fn fields(&self) -> Option<Vec<&str>> {
48		None
49	}
50
51	/// Read-only fields
52	fn readonly_fields(&self) -> Vec<&str> {
53		vec![]
54	}
55
56	/// Ordering for list view (prefix with "-" for descending)
57	fn ordering(&self) -> Vec<&str> {
58		vec!["-id"]
59	}
60
61	/// Number of items per page (None = use site default)
62	fn list_per_page(&self) -> Option<usize> {
63		None
64	}
65
66	/// Check if user has permission to view this model
67	///
68	/// Default implementation allows all access.
69	/// Override this method to implement custom permission checking.
70	async fn has_view_permission(&self, _user: &(dyn std::any::Any + Send + Sync)) -> bool {
71		true
72	}
73
74	/// Check if user has permission to add instances
75	///
76	/// Default implementation allows all access.
77	/// Override this method to implement custom permission checking.
78	async fn has_add_permission(&self, _user: &(dyn std::any::Any + Send + Sync)) -> bool {
79		true
80	}
81
82	/// Check if user has permission to change instances
83	///
84	/// Default implementation allows all access.
85	/// Override this method to implement custom permission checking.
86	async fn has_change_permission(&self, _user: &(dyn std::any::Any + Send + Sync)) -> bool {
87		true
88	}
89
90	/// Check if user has permission to delete instances
91	///
92	/// Default implementation allows all access.
93	/// Override this method to implement custom permission checking.
94	async fn has_delete_permission(&self, _user: &(dyn std::any::Any + Send + Sync)) -> bool {
95		true
96	}
97}
98
99/// Configuration-based model admin implementation
100///
101/// Provides a simple way to configure model admin without implementing the trait.
102///
103/// # Examples
104///
105/// ```
106/// use reinhardt_admin::core::{ModelAdminConfig, ModelAdmin};
107///
108/// let admin = ModelAdminConfig::builder()
109///     .model_name("User")
110///     .list_display(vec!["id", "username", "email"])
111///     .list_filter(vec!["is_active"])
112///     .search_fields(vec!["username", "email"])
113///     .build();
114///
115/// assert_eq!(admin.model_name(), "User");
116/// ```
117#[derive(Debug, Clone)]
118pub struct ModelAdminConfig {
119	model_name: String,
120	table_name: Option<String>,
121	pk_field: String,
122	list_display: Vec<String>,
123	list_filter: Vec<String>,
124	search_fields: Vec<String>,
125	fields: Option<Vec<String>>,
126	readonly_fields: Vec<String>,
127	ordering: Vec<String>,
128	list_per_page: Option<usize>,
129}
130
131impl ModelAdminConfig {
132	/// Create a new model admin configuration
133	///
134	/// # Examples
135	///
136	/// ```
137	/// use reinhardt_admin::core::{ModelAdminConfig, ModelAdmin};
138	///
139	/// let admin = ModelAdminConfig::new("User");
140	/// assert_eq!(admin.model_name(), "User");
141	/// ```
142	pub fn new(model_name: impl Into<String>) -> Self {
143		Self {
144			model_name: model_name.into(),
145			table_name: None,
146			pk_field: "id".into(),
147			list_display: vec!["id".into()],
148			list_filter: vec![],
149			search_fields: vec![],
150			fields: None,
151			readonly_fields: vec![],
152			ordering: vec!["-id".into()],
153			list_per_page: None,
154		}
155	}
156
157	/// Start building a model admin configuration
158	///
159	/// # Examples
160	///
161	/// ```
162	/// use reinhardt_admin::core::ModelAdminConfig;
163	///
164	/// let admin = ModelAdminConfig::builder()
165	///     .model_name("User")
166	///     .list_display(vec!["id", "username"])
167	///     .build();
168	/// ```
169	pub fn builder() -> ModelAdminConfigBuilder {
170		ModelAdminConfigBuilder::default()
171	}
172
173	/// Set list display fields
174	pub fn with_list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
175		self.list_display = fields.into_iter().map(Into::into).collect();
176		self
177	}
178
179	/// Set list filter fields
180	pub fn with_list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
181		self.list_filter = fields.into_iter().map(Into::into).collect();
182		self
183	}
184
185	/// Set search fields
186	pub fn with_search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
187		self.search_fields = fields.into_iter().map(Into::into).collect();
188		self
189	}
190}
191
192#[async_trait]
193impl ModelAdmin for ModelAdminConfig {
194	fn model_name(&self) -> &str {
195		&self.model_name
196	}
197
198	fn table_name(&self) -> &str {
199		self.table_name
200			.as_deref()
201			.unwrap_or(self.model_name.as_str())
202	}
203
204	fn pk_field(&self) -> &str {
205		&self.pk_field
206	}
207
208	fn list_display(&self) -> Vec<&str> {
209		self.list_display.iter().map(|s| s.as_str()).collect()
210	}
211
212	fn list_filter(&self) -> Vec<&str> {
213		self.list_filter.iter().map(|s| s.as_str()).collect()
214	}
215
216	fn search_fields(&self) -> Vec<&str> {
217		self.search_fields.iter().map(|s| s.as_str()).collect()
218	}
219
220	fn fields(&self) -> Option<Vec<&str>> {
221		self.fields
222			.as_ref()
223			.map(|f| f.iter().map(|s| s.as_str()).collect())
224	}
225
226	fn readonly_fields(&self) -> Vec<&str> {
227		self.readonly_fields.iter().map(|s| s.as_str()).collect()
228	}
229
230	fn ordering(&self) -> Vec<&str> {
231		self.ordering.iter().map(|s| s.as_str()).collect()
232	}
233
234	fn list_per_page(&self) -> Option<usize> {
235		self.list_per_page
236	}
237}
238
239/// Builder for ModelAdminConfig
240#[derive(Debug, Default)]
241pub struct ModelAdminConfigBuilder {
242	model_name: Option<String>,
243	table_name: Option<String>,
244	pk_field: Option<String>,
245	list_display: Option<Vec<String>>,
246	list_filter: Option<Vec<String>>,
247	search_fields: Option<Vec<String>>,
248	fields: Option<Vec<String>>,
249	readonly_fields: Option<Vec<String>>,
250	ordering: Option<Vec<String>>,
251	list_per_page: Option<usize>,
252}
253
254impl ModelAdminConfigBuilder {
255	/// Set the model name
256	pub fn model_name(mut self, name: impl Into<String>) -> Self {
257		self.model_name = Some(name.into());
258		self
259	}
260
261	/// Set the database table name
262	///
263	/// If not set, defaults to the model name.
264	pub fn table_name(mut self, name: impl Into<String>) -> Self {
265		self.table_name = Some(name.into());
266		self
267	}
268
269	/// Set the primary key field name
270	///
271	/// If not set, defaults to "id".
272	pub fn pk_field(mut self, field: impl Into<String>) -> Self {
273		self.pk_field = Some(field.into());
274		self
275	}
276
277	/// Set list display fields
278	pub fn list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
279		self.list_display = Some(fields.into_iter().map(Into::into).collect());
280		self
281	}
282
283	/// Set list filter fields
284	pub fn list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
285		self.list_filter = Some(fields.into_iter().map(Into::into).collect());
286		self
287	}
288
289	/// Set search fields
290	pub fn search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
291		self.search_fields = Some(fields.into_iter().map(Into::into).collect());
292		self
293	}
294
295	/// Set form fields
296	pub fn fields(mut self, fields: Vec<impl Into<String>>) -> Self {
297		self.fields = Some(fields.into_iter().map(Into::into).collect());
298		self
299	}
300
301	/// Set readonly fields
302	pub fn readonly_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
303		self.readonly_fields = Some(fields.into_iter().map(Into::into).collect());
304		self
305	}
306
307	/// Set ordering
308	pub fn ordering(mut self, fields: Vec<impl Into<String>>) -> Self {
309		self.ordering = Some(fields.into_iter().map(Into::into).collect());
310		self
311	}
312
313	/// Set items per page
314	pub fn list_per_page(mut self, count: usize) -> Self {
315		self.list_per_page = Some(count);
316		self
317	}
318
319	/// Build the configuration
320	///
321	/// # Panics
322	///
323	/// Panics if model_name is not set
324	pub fn build(self) -> ModelAdminConfig {
325		ModelAdminConfig {
326			model_name: self.model_name.expect("model_name is required"),
327			table_name: self.table_name,
328			pk_field: self.pk_field.unwrap_or_else(|| "id".into()),
329			list_display: self.list_display.unwrap_or_else(|| vec!["id".into()]),
330			list_filter: self.list_filter.unwrap_or_default(),
331			search_fields: self.search_fields.unwrap_or_default(),
332			fields: self.fields,
333			readonly_fields: self.readonly_fields.unwrap_or_default(),
334			ordering: self.ordering.unwrap_or_else(|| vec!["-id".into()]),
335			list_per_page: self.list_per_page,
336		}
337	}
338}
339
340#[cfg(test)]
341mod tests {
342	use super::*;
343	use rstest::rstest;
344
345	#[rstest]
346	fn test_model_admin_config_creation() {
347		let admin = ModelAdminConfig::new("User");
348		assert_eq!(admin.model_name(), "User");
349		assert_eq!(admin.list_display(), vec!["id"]);
350		assert_eq!(admin.list_filter(), Vec::<&str>::new());
351	}
352
353	#[rstest]
354	fn test_model_admin_config_builder() {
355		let admin = ModelAdminConfig::builder()
356			.model_name("User")
357			.list_display(vec!["id", "username", "email"])
358			.list_filter(vec!["is_active"])
359			.search_fields(vec!["username", "email"])
360			.list_per_page(50)
361			.build();
362
363		assert_eq!(admin.model_name(), "User");
364		assert_eq!(admin.list_display(), vec!["id", "username", "email"]);
365		assert_eq!(admin.list_filter(), vec!["is_active"]);
366		assert_eq!(admin.search_fields(), vec!["username", "email"]);
367		assert_eq!(admin.list_per_page(), Some(50));
368	}
369
370	#[rstest]
371	fn test_with_methods() {
372		let admin = ModelAdminConfig::new("Post")
373			.with_list_display(vec!["id", "title", "author"])
374			.with_list_filter(vec!["status", "created_at"])
375			.with_search_fields(vec!["title", "content"]);
376
377		assert_eq!(admin.list_display(), vec!["id", "title", "author"]);
378		assert_eq!(admin.list_filter(), vec!["status", "created_at"]);
379		assert_eq!(admin.search_fields(), vec!["title", "content"]);
380	}
381
382	#[rstest]
383	#[should_panic(expected = "model_name is required")]
384	fn test_builder_without_model_name() {
385		ModelAdminConfig::builder().build();
386	}
387}