1use crate::types::{AdminError, AdminResult};
6use async_trait::async_trait;
7
8pub trait AdminUser: Send + Sync {
21 fn is_active(&self) -> bool;
23
24 fn is_staff(&self) -> bool;
26
27 fn is_superuser(&self) -> bool;
29
30 fn get_username(&self) -> &str;
32}
33
34impl<T: reinhardt_auth::FullUser> AdminUser for T {
39 fn is_active(&self) -> bool {
40 reinhardt_auth::BaseUser::is_active(self)
41 }
42
43 fn is_staff(&self) -> bool {
44 reinhardt_auth::FullUser::is_staff(self)
45 }
46
47 fn is_superuser(&self) -> bool {
48 reinhardt_auth::FullUser::is_superuser(self)
49 }
50
51 fn get_username(&self) -> &str {
52 reinhardt_auth::FullUser::username(self)
53 }
54}
55
56#[async_trait]
60pub trait ModelAdmin: Send + Sync {
61 fn model_name(&self) -> &str;
63
64 fn table_name(&self) -> &str {
69 ""
72 }
73
74 fn pk_field(&self) -> &str {
78 "id"
79 }
80
81 fn list_display(&self) -> Vec<&str> {
83 vec!["id"]
84 }
85
86 fn list_filter(&self) -> Vec<&str> {
88 vec![]
89 }
90
91 fn search_fields(&self) -> Vec<&str> {
93 vec![]
94 }
95
96 fn fields(&self) -> Option<Vec<&str>> {
98 None
99 }
100
101 fn readonly_fields(&self) -> Vec<&str> {
103 vec![]
104 }
105
106 fn ordering(&self) -> Vec<&str> {
108 vec!["-id"]
109 }
110
111 fn list_per_page(&self) -> Option<usize> {
113 None
114 }
115
116 async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
126 false
127 }
128
129 async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
134 false
135 }
136
137 async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
142 false
143 }
144
145 async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
150 false
151 }
152}
153
154#[derive(Debug, Clone)]
175pub struct ModelAdminConfig {
176 model_name: String,
177 table_name: Option<String>,
178 pk_field: String,
179 list_display: Vec<String>,
180 list_filter: Vec<String>,
181 search_fields: Vec<String>,
182 fields: Option<Vec<String>>,
183 readonly_fields: Vec<String>,
184 ordering: Vec<String>,
185 list_per_page: Option<usize>,
186 allow_view: bool,
187 allow_add: bool,
188 allow_change: bool,
189 allow_delete: bool,
190}
191
192impl ModelAdminConfig {
193 pub fn new(model_name: impl Into<String>) -> Self {
204 Self {
205 model_name: model_name.into(),
206 table_name: None,
207 pk_field: "id".into(),
208 list_display: vec!["id".into()],
209 list_filter: vec![],
210 search_fields: vec![],
211 fields: None,
212 readonly_fields: vec![],
213 ordering: vec!["-id".into()],
214 list_per_page: None,
215 allow_view: false,
216 allow_add: false,
217 allow_change: false,
218 allow_delete: false,
219 }
220 }
221
222 pub fn builder() -> ModelAdminConfigBuilder {
236 ModelAdminConfigBuilder::default()
237 }
238
239 pub fn with_list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
241 self.list_display = fields.into_iter().map(Into::into).collect();
242 self
243 }
244
245 pub fn with_list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
247 self.list_filter = fields.into_iter().map(Into::into).collect();
248 self
249 }
250
251 pub fn with_search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
253 self.search_fields = fields.into_iter().map(Into::into).collect();
254 self
255 }
256}
257
258#[async_trait]
259impl ModelAdmin for ModelAdminConfig {
260 fn model_name(&self) -> &str {
261 &self.model_name
262 }
263
264 fn table_name(&self) -> &str {
265 self.table_name
266 .as_deref()
267 .unwrap_or(self.model_name.as_str())
268 }
269
270 fn pk_field(&self) -> &str {
271 &self.pk_field
272 }
273
274 fn list_display(&self) -> Vec<&str> {
275 self.list_display.iter().map(|s| s.as_str()).collect()
276 }
277
278 fn list_filter(&self) -> Vec<&str> {
279 self.list_filter.iter().map(|s| s.as_str()).collect()
280 }
281
282 fn search_fields(&self) -> Vec<&str> {
283 self.search_fields.iter().map(|s| s.as_str()).collect()
284 }
285
286 fn fields(&self) -> Option<Vec<&str>> {
287 self.fields
288 .as_ref()
289 .map(|f| f.iter().map(|s| s.as_str()).collect())
290 }
291
292 fn readonly_fields(&self) -> Vec<&str> {
293 self.readonly_fields.iter().map(|s| s.as_str()).collect()
294 }
295
296 fn ordering(&self) -> Vec<&str> {
297 self.ordering.iter().map(|s| s.as_str()).collect()
298 }
299
300 fn list_per_page(&self) -> Option<usize> {
301 self.list_per_page
302 }
303
304 async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
305 self.allow_view
306 }
307
308 async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
309 self.allow_add
310 }
311
312 async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
313 self.allow_change
314 }
315
316 async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
317 self.allow_delete
318 }
319}
320
321#[derive(Debug, Default)]
323pub struct ModelAdminConfigBuilder {
324 model_name: Option<String>,
325 table_name: Option<String>,
326 pk_field: Option<String>,
327 list_display: Option<Vec<String>>,
328 list_filter: Option<Vec<String>>,
329 search_fields: Option<Vec<String>>,
330 fields: Option<Vec<String>>,
331 readonly_fields: Option<Vec<String>>,
332 ordering: Option<Vec<String>>,
333 list_per_page: Option<usize>,
334 allow_view: Option<bool>,
335 allow_add: Option<bool>,
336 allow_change: Option<bool>,
337 allow_delete: Option<bool>,
338}
339
340impl ModelAdminConfigBuilder {
341 pub fn model_name(mut self, name: impl Into<String>) -> Self {
343 self.model_name = Some(name.into());
344 self
345 }
346
347 pub fn table_name(mut self, name: impl Into<String>) -> Self {
351 self.table_name = Some(name.into());
352 self
353 }
354
355 pub fn pk_field(mut self, field: impl Into<String>) -> Self {
359 self.pk_field = Some(field.into());
360 self
361 }
362
363 pub fn list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
365 self.list_display = Some(fields.into_iter().map(Into::into).collect());
366 self
367 }
368
369 pub fn list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
371 self.list_filter = Some(fields.into_iter().map(Into::into).collect());
372 self
373 }
374
375 pub fn search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
377 self.search_fields = Some(fields.into_iter().map(Into::into).collect());
378 self
379 }
380
381 pub fn fields(mut self, fields: Vec<impl Into<String>>) -> Self {
383 self.fields = Some(fields.into_iter().map(Into::into).collect());
384 self
385 }
386
387 pub fn readonly_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
389 self.readonly_fields = Some(fields.into_iter().map(Into::into).collect());
390 self
391 }
392
393 pub fn ordering(mut self, fields: Vec<impl Into<String>>) -> Self {
395 self.ordering = Some(fields.into_iter().map(Into::into).collect());
396 self
397 }
398
399 pub fn list_per_page(mut self, count: usize) -> Self {
401 self.list_per_page = Some(count);
402 self
403 }
404
405 pub fn allow_view(mut self, allow: bool) -> Self {
409 self.allow_view = Some(allow);
410 self
411 }
412
413 pub fn allow_add(mut self, allow: bool) -> Self {
417 self.allow_add = Some(allow);
418 self
419 }
420
421 pub fn allow_change(mut self, allow: bool) -> Self {
425 self.allow_change = Some(allow);
426 self
427 }
428
429 pub fn allow_delete(mut self, allow: bool) -> Self {
433 self.allow_delete = Some(allow);
434 self
435 }
436
437 pub fn allow_all(mut self, allow: bool) -> Self {
453 self.allow_view = Some(allow);
454 self.allow_add = Some(allow);
455 self.allow_change = Some(allow);
456 self.allow_delete = Some(allow);
457 self
458 }
459
460 pub fn build(self) -> AdminResult<ModelAdminConfig> {
466 let model_name = self
467 .model_name
468 .ok_or_else(|| AdminError::ValidationError("model_name is required".to_string()))?;
469
470 Ok(ModelAdminConfig {
471 model_name,
472 table_name: self.table_name,
473 pk_field: self.pk_field.unwrap_or_else(|| "id".into()),
474 list_display: self.list_display.unwrap_or_else(|| vec!["id".into()]),
475 list_filter: self.list_filter.unwrap_or_default(),
476 search_fields: self.search_fields.unwrap_or_default(),
477 fields: self.fields,
478 readonly_fields: self.readonly_fields.unwrap_or_default(),
479 ordering: self.ordering.unwrap_or_else(|| vec!["-id".into()]),
480 list_per_page: self.list_per_page,
481 allow_view: self.allow_view.unwrap_or(false),
482 allow_add: self.allow_add.unwrap_or(false),
483 allow_change: self.allow_change.unwrap_or(false),
484 allow_delete: self.allow_delete.unwrap_or(false),
485 })
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use rstest::rstest;
493
494 struct TestAdminUser {
496 active: bool,
497 staff: bool,
498 superuser: bool,
499 username: String,
500 }
501
502 impl TestAdminUser {
503 fn new() -> Self {
504 Self {
505 active: true,
506 staff: true,
507 superuser: false,
508 username: "test_user".to_string(),
509 }
510 }
511 }
512
513 impl AdminUser for TestAdminUser {
514 fn is_active(&self) -> bool {
515 self.active
516 }
517
518 fn is_staff(&self) -> bool {
519 self.staff
520 }
521
522 fn is_superuser(&self) -> bool {
523 self.superuser
524 }
525
526 fn get_username(&self) -> &str {
527 &self.username
528 }
529 }
530
531 #[rstest]
532 fn test_model_admin_config_creation() {
533 let admin = ModelAdminConfig::new("User");
534 assert_eq!(admin.model_name(), "User");
535 assert_eq!(admin.list_display(), vec!["id"]);
536 assert_eq!(admin.list_filter(), Vec::<&str>::new());
537 }
538
539 #[rstest]
540 fn test_model_admin_config_builder() {
541 let admin = ModelAdminConfig::builder()
542 .model_name("User")
543 .list_display(vec!["id", "username", "email"])
544 .list_filter(vec!["is_active"])
545 .search_fields(vec!["username", "email"])
546 .list_per_page(50)
547 .build()
548 .unwrap();
549
550 assert_eq!(admin.model_name(), "User");
551 assert_eq!(admin.list_display(), vec!["id", "username", "email"]);
552 assert_eq!(admin.list_filter(), vec!["is_active"]);
553 assert_eq!(admin.search_fields(), vec!["username", "email"]);
554 assert_eq!(admin.list_per_page(), Some(50));
555 }
556
557 #[rstest]
558 fn test_with_methods() {
559 let admin = ModelAdminConfig::new("Post")
560 .with_list_display(vec!["id", "title", "author"])
561 .with_list_filter(vec!["status", "created_at"])
562 .with_search_fields(vec!["title", "content"]);
563
564 assert_eq!(admin.list_display(), vec!["id", "title", "author"]);
565 assert_eq!(admin.list_filter(), vec!["status", "created_at"]);
566 assert_eq!(admin.search_fields(), vec!["title", "content"]);
567 }
568
569 #[rstest]
570 fn test_builder_without_model_name_returns_error() {
571 let result = ModelAdminConfig::builder().build();
573
574 assert!(result.is_err());
576 let err = result.unwrap_err();
577 assert!(err.to_string().contains("model_name is required"));
578 }
579
580 struct DefaultPermissionAdmin;
582
583 #[async_trait]
584 impl ModelAdmin for DefaultPermissionAdmin {
585 fn model_name(&self) -> &str {
586 "TestModel"
587 }
588 }
589
590 struct AllowAllPermissionAdmin;
592
593 #[async_trait]
594 impl ModelAdmin for AllowAllPermissionAdmin {
595 fn model_name(&self) -> &str {
596 "AllowedModel"
597 }
598
599 async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
600 true
601 }
602
603 async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
604 true
605 }
606
607 async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
608 true
609 }
610
611 async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
612 true
613 }
614 }
615
616 #[rstest]
617 #[tokio::test]
618 async fn test_default_permissions_deny_view() {
619 let admin = DefaultPermissionAdmin;
621 let user = TestAdminUser::new();
622
623 let result = admin.has_view_permission(&user as &dyn AdminUser).await;
625
626 assert_eq!(result, false);
628 }
629
630 #[rstest]
631 #[tokio::test]
632 async fn test_default_permissions_deny_add() {
633 let admin = DefaultPermissionAdmin;
635 let user = TestAdminUser::new();
636
637 let result = admin.has_add_permission(&user as &dyn AdminUser).await;
639
640 assert_eq!(result, false);
642 }
643
644 #[rstest]
645 #[tokio::test]
646 async fn test_default_permissions_deny_change() {
647 let admin = DefaultPermissionAdmin;
649 let user = TestAdminUser::new();
650
651 let result = admin.has_change_permission(&user as &dyn AdminUser).await;
653
654 assert_eq!(result, false);
656 }
657
658 #[rstest]
659 #[tokio::test]
660 async fn test_default_permissions_deny_delete() {
661 let admin = DefaultPermissionAdmin;
663 let user = TestAdminUser::new();
664
665 let result = admin.has_delete_permission(&user as &dyn AdminUser).await;
667
668 assert_eq!(result, false);
670 }
671
672 #[rstest]
673 #[tokio::test]
674 async fn test_explicit_override_grants_all_permissions() {
675 let admin = AllowAllPermissionAdmin;
677 let user = TestAdminUser::new();
678
679 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
681 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
682 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
683 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
684
685 assert_eq!(view, true);
687 assert_eq!(add, true);
688 assert_eq!(change, true);
689 assert_eq!(delete, true);
690 }
691
692 #[rstest]
693 #[tokio::test]
694 async fn test_model_admin_config_inherits_deny_by_default() {
695 let admin = ModelAdminConfig::new("User");
697 let user = TestAdminUser::new();
698
699 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
701 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
702 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
703 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
704
705 assert_eq!(view, false);
707 assert_eq!(add, false);
708 assert_eq!(change, false);
709 assert_eq!(delete, false);
710 }
711
712 #[rstest]
715 fn test_model_admin_config_custom_pk_field() {
716 let admin = ModelAdminConfig::builder()
718 .model_name("User")
719 .pk_field("uuid")
720 .build()
721 .unwrap();
722
723 let pk = admin.pk_field();
725
726 assert_eq!(pk, "uuid");
728 }
729
730 #[rstest]
731 fn test_model_admin_config_default_pk_field() {
732 let admin = ModelAdminConfig::builder()
734 .model_name("User")
735 .build()
736 .unwrap();
737
738 let pk = admin.pk_field();
740
741 assert_eq!(pk, "id");
743 }
744
745 #[rstest]
746 fn test_model_admin_config_custom_table_name() {
747 let admin = ModelAdminConfig::builder()
749 .model_name("User")
750 .table_name("my_users")
751 .build()
752 .unwrap();
753
754 let table = admin.table_name();
756
757 assert_eq!(table, "my_users");
759 }
760
761 #[rstest]
762 fn test_model_admin_config_table_name_defaults_to_model_name() {
763 let admin = ModelAdminConfig::builder()
765 .model_name("User")
766 .build()
767 .unwrap();
768
769 let table = admin.table_name();
771
772 assert_eq!(table, "User");
774 }
775
776 #[rstest]
777 #[tokio::test]
778 async fn test_model_admin_config_builder_inherits_deny_by_default() {
779 let admin = ModelAdminConfig::builder()
781 .model_name("Post")
782 .list_display(vec!["id", "title"])
783 .build()
784 .unwrap();
785 let user = TestAdminUser::new();
786
787 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
789 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
790
791 assert_eq!(view, false);
793 assert_eq!(add, false);
794 }
795
796 #[rstest]
797 #[tokio::test]
798 async fn test_builder_allow_view_grants_view_permission() {
799 let admin = ModelAdminConfig::builder()
801 .model_name("Post")
802 .allow_view(true)
803 .build()
804 .unwrap();
805 let user = TestAdminUser::new();
806
807 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
809 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
810
811 assert_eq!(view, true);
813 assert_eq!(add, false);
814 }
815
816 #[rstest]
817 #[tokio::test]
818 async fn test_builder_allow_all_grants_all_permissions() {
819 let admin = ModelAdminConfig::builder()
821 .model_name("Post")
822 .allow_all(true)
823 .build()
824 .unwrap();
825 let user = TestAdminUser::new();
826
827 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
829 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
830 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
831 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
832
833 assert_eq!(view, true);
835 assert_eq!(add, true);
836 assert_eq!(change, true);
837 assert_eq!(delete, true);
838 }
839
840 #[rstest]
841 #[tokio::test]
842 async fn test_builder_allow_all_false_denies_all() {
843 let admin = ModelAdminConfig::builder()
845 .model_name("Post")
846 .allow_all(false)
847 .build()
848 .unwrap();
849 let user = TestAdminUser::new();
850
851 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
853 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
854
855 assert_eq!(view, false);
857 assert_eq!(add, false);
858 }
859
860 #[rstest]
861 #[tokio::test]
862 async fn test_builder_individual_permissions() {
863 let admin = ModelAdminConfig::builder()
865 .model_name("Post")
866 .allow_view(true)
867 .allow_add(true)
868 .allow_change(false)
869 .allow_delete(false)
870 .build()
871 .unwrap();
872 let user = TestAdminUser::new();
873
874 let view = admin.has_view_permission(&user as &dyn AdminUser).await;
876 let add = admin.has_add_permission(&user as &dyn AdminUser).await;
877 let change = admin.has_change_permission(&user as &dyn AdminUser).await;
878 let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
879
880 assert_eq!(view, true);
882 assert_eq!(add, true);
883 assert_eq!(change, false);
884 assert_eq!(delete, false);
885 }
886
887 #[rstest]
890 #[case::allow_all_true(true, true)]
891 #[case::allow_all_false(false, false)]
892 #[tokio::test]
893 async fn test_allow_all_controls_view_permission(
894 #[case] allow_all: bool,
895 #[case] expected: bool,
896 ) {
897 let admin = ModelAdminConfig::builder()
899 .model_name("PermTest")
900 .allow_all(allow_all)
901 .build()
902 .unwrap();
903 let user = TestAdminUser::new();
904
905 let result = admin.has_view_permission(&user as &dyn AdminUser).await;
907
908 assert_eq!(result, expected);
910 }
911
912 #[rstest]
915 #[case::with_list_per_page(Some(50), Some(50))]
916 #[case::without_list_per_page(None, None)]
917 fn test_list_per_page_override(
918 #[case] override_value: Option<usize>,
919 #[case] expected: Option<usize>,
920 ) {
921 let mut builder = ModelAdminConfig::builder().model_name("PageTest");
923 if let Some(v) = override_value {
924 builder = builder.list_per_page(v);
925 }
926 let admin = builder.build().unwrap();
927
928 let result = admin.list_per_page();
930
931 assert_eq!(result, expected);
933 }
934
935 #[rstest]
938 #[case::missing_model_name(true)]
939 #[case::valid_model_name(false)]
940 fn test_builder_model_name_validation(#[case] should_error: bool) {
941 let builder = if should_error {
943 ModelAdminConfig::builder()
945 } else {
946 ModelAdminConfig::builder().model_name("User")
947 };
948
949 let result = builder.build();
951
952 assert_eq!(
954 result.is_err(),
955 should_error,
956 "should_error={}, got {:?}",
957 should_error,
958 result
959 );
960 }
961}