1use crate::types::{AdminError, AdminResult};
6use async_trait::async_trait;
7use reinhardt_db::orm::Filter;
8
9pub trait AdminUser: Send + Sync {
22 fn is_active(&self) -> bool;
24
25 fn is_staff(&self) -> bool;
27
28 fn is_superuser(&self) -> bool;
30
31 fn get_username(&self) -> &str;
33}
34
35impl<T: reinhardt_auth::FullUser> AdminUser for T {
40 fn is_active(&self) -> bool {
41 reinhardt_auth::BaseUser::is_active(self)
42 }
43
44 fn is_staff(&self) -> bool {
45 reinhardt_auth::FullUser::is_staff(self)
46 }
47
48 fn is_superuser(&self) -> bool {
49 reinhardt_auth::FullUser::is_superuser(self)
50 }
51
52 fn get_username(&self) -> &str {
53 reinhardt_auth::FullUser::username(self)
54 }
55}
56
57#[async_trait]
61pub trait ModelAdmin: Send + Sync {
62 fn model_name(&self) -> &str;
64
65 fn table_name(&self) -> &str {
70 ""
73 }
74
75 fn pk_field(&self) -> &str {
79 "id"
80 }
81
82 fn list_display(&self) -> Vec<&str> {
84 vec!["id"]
85 }
86
87 fn list_filter(&self) -> Vec<&str> {
89 vec![]
90 }
91
92 fn search_fields(&self) -> Vec<&str> {
94 vec![]
95 }
96
97 fn fields(&self) -> Option<Vec<&str>> {
99 None
100 }
101
102 fn readonly_fields(&self) -> Vec<&str> {
104 vec![]
105 }
106
107 fn ordering(&self) -> Vec<&str> {
109 vec!["-id"]
110 }
111
112 fn list_per_page(&self) -> Option<usize> {
114 None
115 }
116
117 async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
127 false
128 }
129
130 async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
135 false
136 }
137
138 async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
143 false
144 }
145
146 async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
151 false
152 }
153
154 fn object_filters(&self, _user: &dyn AdminUser) -> Option<Vec<Filter>> {
159 None
160 }
161}
162
163#[derive(Debug, Clone)]
184pub struct ModelAdminConfig {
185 model_name: String,
186 table_name: Option<String>,
187 pk_field: String,
188 list_display: Vec<String>,
189 list_filter: Vec<String>,
190 search_fields: Vec<String>,
191 fields: Option<Vec<String>>,
192 readonly_fields: Vec<String>,
193 ordering: Vec<String>,
194 list_per_page: Option<usize>,
195 allow_view: bool,
196 allow_add: bool,
197 allow_change: bool,
198 allow_delete: bool,
199}
200
201impl ModelAdminConfig {
202 pub fn new(model_name: impl Into<String>) -> Self {
213 Self {
214 model_name: model_name.into(),
215 table_name: None,
216 pk_field: "id".into(),
217 list_display: vec!["id".into()],
218 list_filter: vec![],
219 search_fields: vec![],
220 fields: None,
221 readonly_fields: vec![],
222 ordering: vec!["-id".into()],
223 list_per_page: None,
224 allow_view: false,
225 allow_add: false,
226 allow_change: false,
227 allow_delete: false,
228 }
229 }
230
231 pub fn builder() -> ModelAdminConfigBuilder {
245 ModelAdminConfigBuilder::default()
246 }
247
248 pub fn with_list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
250 self.list_display = fields.into_iter().map(Into::into).collect();
251 self
252 }
253
254 pub fn with_list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
256 self.list_filter = fields.into_iter().map(Into::into).collect();
257 self
258 }
259
260 pub fn with_search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
262 self.search_fields = fields.into_iter().map(Into::into).collect();
263 self
264 }
265}
266
267#[async_trait]
268impl ModelAdmin for ModelAdminConfig {
269 fn model_name(&self) -> &str {
270 &self.model_name
271 }
272
273 fn table_name(&self) -> &str {
274 self.table_name
275 .as_deref()
276 .unwrap_or(self.model_name.as_str())
277 }
278
279 fn pk_field(&self) -> &str {
280 &self.pk_field
281 }
282
283 fn list_display(&self) -> Vec<&str> {
284 self.list_display.iter().map(|s| s.as_str()).collect()
285 }
286
287 fn list_filter(&self) -> Vec<&str> {
288 self.list_filter.iter().map(|s| s.as_str()).collect()
289 }
290
291 fn search_fields(&self) -> Vec<&str> {
292 self.search_fields.iter().map(|s| s.as_str()).collect()
293 }
294
295 fn fields(&self) -> Option<Vec<&str>> {
296 self.fields
297 .as_ref()
298 .map(|f| f.iter().map(|s| s.as_str()).collect())
299 }
300
301 fn readonly_fields(&self) -> Vec<&str> {
302 self.readonly_fields.iter().map(|s| s.as_str()).collect()
303 }
304
305 fn ordering(&self) -> Vec<&str> {
306 self.ordering.iter().map(|s| s.as_str()).collect()
307 }
308
309 fn list_per_page(&self) -> Option<usize> {
310 self.list_per_page
311 }
312
313 async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
314 self.allow_view
315 }
316
317 async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
318 self.allow_add
319 }
320
321 async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
322 self.allow_change
323 }
324
325 async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
326 self.allow_delete
327 }
328
329 fn object_filters(&self, _user: &dyn AdminUser) -> Option<Vec<Filter>> {
330 Some(Vec::new())
331 }
332}
333
334#[derive(Debug, Default)]
336pub struct ModelAdminConfigBuilder {
337 model_name: Option<String>,
338 table_name: Option<String>,
339 pk_field: Option<String>,
340 list_display: Option<Vec<String>>,
341 list_filter: Option<Vec<String>>,
342 search_fields: Option<Vec<String>>,
343 fields: Option<Vec<String>>,
344 readonly_fields: Option<Vec<String>>,
345 ordering: Option<Vec<String>>,
346 list_per_page: Option<usize>,
347 allow_view: Option<bool>,
348 allow_add: Option<bool>,
349 allow_change: Option<bool>,
350 allow_delete: Option<bool>,
351}
352
353impl ModelAdminConfigBuilder {
354 pub fn model_name(mut self, name: impl Into<String>) -> Self {
356 self.model_name = Some(name.into());
357 self
358 }
359
360 pub fn table_name(mut self, name: impl Into<String>) -> Self {
364 self.table_name = Some(name.into());
365 self
366 }
367
368 pub fn pk_field(mut self, field: impl Into<String>) -> Self {
372 self.pk_field = Some(field.into());
373 self
374 }
375
376 pub fn list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
378 self.list_display = Some(fields.into_iter().map(Into::into).collect());
379 self
380 }
381
382 pub fn list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
384 self.list_filter = Some(fields.into_iter().map(Into::into).collect());
385 self
386 }
387
388 pub fn search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
390 self.search_fields = Some(fields.into_iter().map(Into::into).collect());
391 self
392 }
393
394 pub fn fields(mut self, fields: Vec<impl Into<String>>) -> Self {
396 self.fields = Some(fields.into_iter().map(Into::into).collect());
397 self
398 }
399
400 pub fn readonly_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
402 self.readonly_fields = Some(fields.into_iter().map(Into::into).collect());
403 self
404 }
405
406 pub fn ordering(mut self, fields: Vec<impl Into<String>>) -> Self {
408 self.ordering = Some(fields.into_iter().map(Into::into).collect());
409 self
410 }
411
412 pub fn list_per_page(mut self, count: usize) -> Self {
414 self.list_per_page = Some(count);
415 self
416 }
417
418 pub fn allow_view(mut self, allow: bool) -> Self {
422 self.allow_view = Some(allow);
423 self
424 }
425
426 pub fn allow_add(mut self, allow: bool) -> Self {
430 self.allow_add = Some(allow);
431 self
432 }
433
434 pub fn allow_change(mut self, allow: bool) -> Self {
438 self.allow_change = Some(allow);
439 self
440 }
441
442 pub fn allow_delete(mut self, allow: bool) -> Self {
446 self.allow_delete = Some(allow);
447 self
448 }
449
450 pub fn allow_all(mut self, allow: bool) -> Self {
466 self.allow_view = Some(allow);
467 self.allow_add = Some(allow);
468 self.allow_change = Some(allow);
469 self.allow_delete = Some(allow);
470 self
471 }
472
473 pub fn build(self) -> AdminResult<ModelAdminConfig> {
479 let model_name = self
480 .model_name
481 .ok_or_else(|| AdminError::ValidationError("model_name is required".to_string()))?;
482
483 Ok(ModelAdminConfig {
484 model_name,
485 table_name: self.table_name,
486 pk_field: self.pk_field.unwrap_or_else(|| "id".into()),
487 list_display: self.list_display.unwrap_or_else(|| vec!["id".into()]),
488 list_filter: self.list_filter.unwrap_or_default(),
489 search_fields: self.search_fields.unwrap_or_default(),
490 fields: self.fields,
491 readonly_fields: self.readonly_fields.unwrap_or_default(),
492 ordering: self.ordering.unwrap_or_else(|| vec!["-id".into()]),
493 list_per_page: self.list_per_page,
494 allow_view: self.allow_view.unwrap_or(false),
495 allow_add: self.allow_add.unwrap_or(false),
496 allow_change: self.allow_change.unwrap_or(false),
497 allow_delete: self.allow_delete.unwrap_or(false),
498 })
499 }
500}
501
502#[cfg(all(test, server))]
503mod tests {
504 use super::*;
505 use rstest::rstest;
506
507 struct TestAdminUser {
509 active: bool,
510 staff: bool,
511 superuser: bool,
512 username: String,
513 }
514
515 impl TestAdminUser {
516 fn new() -> Self {
517 Self {
518 active: true,
519 staff: true,
520 superuser: false,
521 username: "test_user".to_string(),
522 }
523 }
524 }
525
526 impl AdminUser for TestAdminUser {
527 fn is_active(&self) -> bool {
528 self.active
529 }
530
531 fn is_staff(&self) -> bool {
532 self.staff
533 }
534
535 fn is_superuser(&self) -> bool {
536 self.superuser
537 }
538
539 fn get_username(&self) -> &str {
540 &self.username
541 }
542 }
543
544 #[rstest]
545 fn test_model_admin_config_creation() {
546 let admin = ModelAdminConfig::new("User");
547 assert_eq!(admin.model_name(), "User");
548 assert_eq!(admin.list_display(), vec!["id"]);
549 assert_eq!(admin.list_filter(), Vec::<&str>::new());
550 }
551
552 #[rstest]
553 fn test_model_admin_config_builder() {
554 let admin = ModelAdminConfig::builder()
555 .model_name("User")
556 .list_display(vec!["id", "username", "email"])
557 .list_filter(vec!["is_active"])
558 .search_fields(vec!["username", "email"])
559 .list_per_page(50)
560 .build()
561 .unwrap();
562
563 assert_eq!(admin.model_name(), "User");
564 assert_eq!(admin.list_display(), vec!["id", "username", "email"]);
565 assert_eq!(admin.list_filter(), vec!["is_active"]);
566 assert_eq!(admin.search_fields(), vec!["username", "email"]);
567 assert_eq!(admin.list_per_page(), Some(50));
568 }
569
570 #[rstest]
571 fn test_with_methods() {
572 let admin = ModelAdminConfig::new("Post")
573 .with_list_display(vec!["id", "title", "author"])
574 .with_list_filter(vec!["status", "created_at"])
575 .with_search_fields(vec!["title", "content"]);
576
577 assert_eq!(admin.list_display(), vec!["id", "title", "author"]);
578 assert_eq!(admin.list_filter(), vec!["status", "created_at"]);
579 assert_eq!(admin.search_fields(), vec!["title", "content"]);
580 }
581
582 #[rstest]
583 fn test_builder_without_model_name_returns_error() {
584 let result = ModelAdminConfig::builder().build();
586
587 assert!(result.is_err());
589 let err = result.unwrap_err();
590 assert!(err.to_string().contains("model_name is required"));
591 }
592
593 struct DefaultPermissionAdmin;
595
596 #[async_trait]
597 impl ModelAdmin for DefaultPermissionAdmin {
598 fn model_name(&self) -> &str {
599 "TestModel"
600 }
601 }
602
603 struct AllowAllPermissionAdmin;
605
606 #[async_trait]
607 impl ModelAdmin for AllowAllPermissionAdmin {
608 fn model_name(&self) -> &str {
609 "AllowedModel"
610 }
611
612 async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
613 true
614 }
615
616 async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
617 true
618 }
619
620 async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
621 true
622 }
623
624 async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
625 true
626 }
627 }
628
629 #[rstest]
630 #[tokio::test]
631 async fn test_default_permissions_deny_view() {
632 let admin = DefaultPermissionAdmin;
634 let user = TestAdminUser::new();
635
636 let result = admin.has_view_permission(&user as &dyn AdminUser).await;
638
639 assert_eq!(result, false);
641 }
642
643 #[rstest]
644 #[tokio::test]
645 async fn test_default_permissions_deny_add() {
646 let admin = DefaultPermissionAdmin;
648 let user = TestAdminUser::new();
649
650 let result = admin.has_add_permission(&user as &dyn AdminUser).await;
652
653 assert_eq!(result, false);
655 }
656
657 #[rstest]
658 #[tokio::test]
659 async fn test_default_permissions_deny_change() {
660 let admin = DefaultPermissionAdmin;
662 let user = TestAdminUser::new();
663
664 let result = admin.has_change_permission(&user as &dyn AdminUser).await;
666
667 assert_eq!(result, false);
669 }
670
671 #[rstest]
672 #[tokio::test]
673 async fn test_default_permissions_deny_delete() {
674 let admin = DefaultPermissionAdmin;
676 let user = TestAdminUser::new();
677
678 let result = admin.has_delete_permission(&user as &dyn AdminUser).await;
680
681 assert_eq!(result, false);
683 }
684
685 #[rstest]
686 #[tokio::test]
687 async fn test_explicit_override_grants_all_permissions() {
688 let admin = AllowAllPermissionAdmin;
690 let user = TestAdminUser::new();
691
692 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
694 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
695 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
696 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
697
698 assert_eq!(view, true);
700 assert_eq!(add, true);
701 assert_eq!(change, true);
702 assert_eq!(delete, true);
703 }
704
705 #[rstest]
706 #[tokio::test]
707 async fn test_model_admin_config_inherits_deny_by_default() {
708 let admin = ModelAdminConfig::new("User");
710 let user = TestAdminUser::new();
711
712 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
714 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
715 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
716 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
717
718 assert_eq!(view, false);
720 assert_eq!(add, false);
721 assert_eq!(change, false);
722 assert_eq!(delete, false);
723 }
724
725 #[rstest]
728 fn test_model_admin_config_custom_pk_field() {
729 let admin = ModelAdminConfig::builder()
731 .model_name("User")
732 .pk_field("uuid")
733 .build()
734 .unwrap();
735
736 let pk = admin.pk_field();
738
739 assert_eq!(pk, "uuid");
741 }
742
743 #[rstest]
744 fn test_model_admin_config_default_pk_field() {
745 let admin = ModelAdminConfig::builder()
747 .model_name("User")
748 .build()
749 .unwrap();
750
751 let pk = admin.pk_field();
753
754 assert_eq!(pk, "id");
756 }
757
758 #[rstest]
759 fn test_model_admin_config_custom_table_name() {
760 let admin = ModelAdminConfig::builder()
762 .model_name("User")
763 .table_name("my_users")
764 .build()
765 .unwrap();
766
767 let table = admin.table_name();
769
770 assert_eq!(table, "my_users");
772 }
773
774 #[rstest]
775 fn test_model_admin_config_table_name_defaults_to_model_name() {
776 let admin = ModelAdminConfig::builder()
778 .model_name("User")
779 .build()
780 .unwrap();
781
782 let table = admin.table_name();
784
785 assert_eq!(table, "User");
787 }
788
789 #[rstest]
790 #[tokio::test]
791 async fn test_model_admin_config_builder_inherits_deny_by_default() {
792 let admin = ModelAdminConfig::builder()
794 .model_name("Post")
795 .list_display(vec!["id", "title"])
796 .build()
797 .unwrap();
798 let user = TestAdminUser::new();
799
800 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
802 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
803
804 assert_eq!(view, false);
806 assert_eq!(add, false);
807 }
808
809 #[rstest]
810 #[tokio::test]
811 async fn test_builder_allow_view_grants_view_permission() {
812 let admin = ModelAdminConfig::builder()
814 .model_name("Post")
815 .allow_view(true)
816 .build()
817 .unwrap();
818 let user = TestAdminUser::new();
819
820 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
822 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
823
824 assert_eq!(view, true);
826 assert_eq!(add, false);
827 }
828
829 #[rstest]
830 #[tokio::test]
831 async fn test_builder_allow_all_grants_all_permissions() {
832 let admin = ModelAdminConfig::builder()
834 .model_name("Post")
835 .allow_all(true)
836 .build()
837 .unwrap();
838 let user = TestAdminUser::new();
839
840 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
842 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
843 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
844 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
845
846 assert_eq!(view, true);
848 assert_eq!(add, true);
849 assert_eq!(change, true);
850 assert_eq!(delete, true);
851 }
852
853 #[rstest]
854 #[tokio::test]
855 async fn test_builder_allow_all_false_denies_all() {
856 let admin = ModelAdminConfig::builder()
858 .model_name("Post")
859 .allow_all(false)
860 .build()
861 .unwrap();
862 let user = TestAdminUser::new();
863
864 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
866 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
867
868 assert_eq!(view, false);
870 assert_eq!(add, false);
871 }
872
873 #[rstest]
874 #[tokio::test]
875 async fn test_builder_individual_permissions() {
876 let admin = ModelAdminConfig::builder()
878 .model_name("Post")
879 .allow_view(true)
880 .allow_add(true)
881 .allow_change(false)
882 .allow_delete(false)
883 .build()
884 .unwrap();
885 let user = TestAdminUser::new();
886
887 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
889 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
890 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
891 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
892
893 assert_eq!(view, true);
895 assert_eq!(add, true);
896 assert_eq!(change, false);
897 assert_eq!(delete, false);
898 }
899
900 #[rstest]
903 #[case::allow_all_true(true, true)]
904 #[case::allow_all_false(false, false)]
905 #[tokio::test]
906 async fn test_allow_all_controls_view_permission(
907 #[case] allow_all: bool,
908 #[case] expected: bool,
909 ) {
910 let admin = ModelAdminConfig::builder()
912 .model_name("PermTest")
913 .allow_all(allow_all)
914 .build()
915 .unwrap();
916 let user = TestAdminUser::new();
917
918 let result = admin.has_view_permission(&user as &dyn AdminUser).await;
920
921 assert_eq!(result, expected);
923 }
924
925 #[rstest]
928 #[case::with_list_per_page(Some(50), Some(50))]
929 #[case::without_list_per_page(None, None)]
930 fn test_list_per_page_override(
931 #[case] override_value: Option<usize>,
932 #[case] expected: Option<usize>,
933 ) {
934 let mut builder = ModelAdminConfig::builder().model_name("PageTest");
936 if let Some(v) = override_value {
937 builder = builder.list_per_page(v);
938 }
939 let admin = builder.build().unwrap();
940
941 let result = admin.list_per_page();
943
944 assert_eq!(result, expected);
946 }
947
948 #[rstest]
951 #[case::missing_model_name(true)]
952 #[case::valid_model_name(false)]
953 fn test_builder_model_name_validation(#[case] should_error: bool) {
954 let builder = if should_error {
956 ModelAdminConfig::builder()
958 } else {
959 ModelAdminConfig::builder().model_name("User")
960 };
961
962 let result = builder.build();
964
965 assert_eq!(
967 result.is_err(),
968 should_error,
969 "should_error={}, got {:?}",
970 should_error,
971 result
972 );
973 }
974}