Coverage for admin.py: 96%

1 import contextlib

2 from collections import namedtuple

3 from collections.abc import Iterable

4 from typing import TYPE_CHECKING, List, Type, Union

5

6 from django.apps import apps

7 from django.conf import settings

8 from django.contrib import admin

9 from django.contrib.admin.apps import AdminConfig

10 from django.core.exceptions import ImproperlyConfigured

11 from django.urls import include, path, reverse

12 from django.views import View

13

14 from django_custom_admin_pages.urls import add_view_to_conf

15

16 if TYPE_CHECKING:

17 from .views.admin_base_view import AdminBaseView

18

19

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

21

22

23 def get_installed_apps():

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

25 return installed_apps

26

27

28 def 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

33 class CustomAdminConfig(AdminConfig):

34 default_site = "django_custom_admin_pages.admin.CustomAdminSite"

35

36

37 class 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