Coverage for admin.py: 96%

101 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-08-03 17:57 -0700

1import contextlib 

2from collections import namedtuple 

3from collections.abc import Iterable 

4from typing import TYPE_CHECKING, List, Type, Union 

5 

6from django.apps import apps 

7from django.conf import settings 

8from django.contrib import admin 

9from django.contrib.admin.apps import AdminConfig 

10from django.core.exceptions import ImproperlyConfigured 

11from django.urls import include, path, reverse 

12from django.views import View 

13 

14from django_custom_admin_pages.urls import add_view_to_conf 

15 

16if TYPE_CHECKING: 

17 from .views.admin_base_view import AdminBaseView 

18 

19 

20ViewRegister = namedtuple("ViewRegister", ["app_label", "view"]) 

21 

22 

23def get_installed_apps(): 

24 installed_apps = [app_config.name for app_config in apps.get_app_configs()] 

25 return installed_apps 

26 

27 

28def get_app_label(view: View) -> str: 

29 "returns app label or default app for view" 

30 return getattr(view, "app_label") or settings.CUSTOM_ADMIN_DEFAULT_APP_LABEL 

31 

32 

33class CustomAdminConfig(AdminConfig): 

34 default_site = "django_custom_admin_pages.admin.CustomAdminSite" 

35 

36 

37class CustomAdminSite(admin.AdminSite): 

38 def __init__(self, *args, **kwargs): 

39 self._view_registry: List["AdminBaseView"] = [] 

40 super().__init__(*args, **kwargs) 

41 

42 def get_urls(self): 

43 """ 

44 Adds urls.py to perch-admin routes if url patterns in it 

45 """ 

46 urls = super().get_urls() 

47 with contextlib.suppress(ImportError): 

48 from .urls import urlpatterns 

49 

50 if len(urlpatterns) > 0: 

51 my_urls = [ 

52 path( 

53 "", 

54 include("django_custom_admin_pages.urls"), 

55 ) 

56 ] 

57 urls = my_urls + urls 

58 

59 return urls 

60 

61 def register_view(self, view_or_iterable: Union[Iterable, "AdminBaseView"]): 

62 """ 

63 Register the given view(s) with the admin class. 

64 

65 The view(s) should be class-based views inheriting from AdminBaseView. 

66 

67 If the view is already registered, raise AlreadyRegistered. 

68 """ 

69 from .views.admin_base_view import AdminBaseView 

70 

71 if not isinstance(view_or_iterable, Iterable): 

72 view_or_iterable = [view_or_iterable] 

73 

74 for view in view_or_iterable: 

75 try: 

76 if not issubclass(view, AdminBaseView): 

77 raise ImproperlyConfigured( 

78 "Only class-based views inheriting from AdminBaseView can be registered" 

79 ) 

80 except TypeError as e: 

81 raise ImproperlyConfigured( 

82 "view_or_iterable must be a class_based view or iterable" 

83 ) from e 

84 

85 if ( 

86 not hasattr(view, "view_name") 

87 or view.view_name is None 

88 or not isinstance(view.view_name, str) 

89 ): 

90 raise ImproperlyConfigured( 

91 "View must have name attribute set as string." 

92 ) 

93 

94 if view in self._view_registry: 

95 raise admin.sites.AlreadyRegistered( 

96 f"View: {str(view.view_name)} is already registered." 

97 ) 

98 

99 self._view_registry.append(view) 

100 

101 add_view_to_conf(view) 

102 

103 def unregister_view(self, view_or_iterable: Union[Iterable, Type]): 

104 def _raise_not_registered(view, e=None): 

105 msg = f"The view {view.__name__} is not registered" 

106 if e: 

107 raise admin.sites.NotRegistered(msg) from e 

108 raise admin.sites.NotRegistered(msg) 

109 

110 if not isinstance(view_or_iterable, Iterable): 

111 view_or_iterable = [view_or_iterable] 

112 

113 original_length = len(self._view_registry) 

114 

115 for view in view_or_iterable: 

116 try: 

117 self._view_registry.remove(view) 

118 except ValueError as e: 

119 _raise_not_registered(view, e) 

120 

121 if len(self._view_registry) == original_length: 

122 _raise_not_registered(view) 

123 

124 def _build_modelview(self, view) -> dict: 

125 """ 

126 Creates dict for custom admin view for use in app_list[models] 

127 """ 

128 url = reverse(f"{self.name}:{view.route_name}") 

129 name = view.view_name 

130 return { 

131 "name": name, 

132 "object_name": name, 

133 "admin_url": url, 

134 "view_only": True, 

135 } 

136 

137 def get_app_list(self, request): 

138 """ 

139 Adds registered apps to admin app list used for nav. 

140 """ 

141 app_list = super().get_app_list(request) 

142 custom_admin_models = [] 

143 

144 for view in self._view_registry: 

145 found = False 

146 view_app_label = get_app_label(view).lower() 

147 if view_app_label == settings.CUSTOM_ADMIN_DEFAULT_APP_LABEL: 

148 custom_admin_models.append(self._build_modelview(view)) 

149 continue 

150 

151 for app in app_list: 

152 if view_app_label == app.get("app_label", "").lower(): 

153 found = True 

154 if view().user_has_permission(request.user): 

155 app_models = app["models"] 

156 app_models.append(self._build_modelview(view)) 

157 app_models.sort(key=lambda x: x["name"]) 

158 break 

159 

160 if not found: 

161 remaining_apps = set(set(get_installed_apps())).difference(app_list) 

162 for app in remaining_apps: 

163 if view_app_label == app: 

164 found = True 

165 app_config = apps.get_app_config(view_app_label) 

166 app_name = app_config.verbose_name 

167 app_list.append( 

168 { 

169 "name": app_name, 

170 "app_label": view_app_label, 

171 "app_url": f"{reverse(f'{self.name}:{view.route_name}')}{view_app_label}/", 

172 "models": [self._build_modelview(view)], 

173 } 

174 ) 

175 

176 if not found: 

177 raise ImproperlyConfigured( 

178 f'The following custom admin view has an app_label that couldn\'t be found: "{view.__name__}". Please check that "{view_app_label}" is a valid app_label.' 

179 ) 

180 

181 if custom_admin_models: 

182 app_list += [ 

183 { 

184 "name": "Custom Admin Pages", 

185 "app_label": settings.CUSTOM_ADMIN_DEFAULT_APP_LABEL, 

186 "app_url": f"{reverse(f'{self.name}:index')}{settings.CUSTOM_ADMIN_DEFAULT_APP_LABEL}/", 

187 "models": custom_admin_models, 

188 } 

189 ] 

190 app_list = sorted(app_list, key=lambda x: x["name"]) 

191 return app_list