Coverage for crateweb/extra/admin.py: 55%

75 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-27 10:34 -0500

1""" 

2crate_anon/crateweb/extra/admin.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CRATE. 

10 

11 CRATE is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CRATE is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CRATE. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26**Extensions to Django admin site classes.** 

27 

28""" 

29 

30import logging 

31from typing import Any, Dict, List, Type 

32 

33from django.contrib.admin import ModelAdmin 

34from django.contrib.admin.views.main import ChangeList 

35from django.forms import ModelForm 

36from django.http import HttpResponse 

37from django.http.request import HttpRequest 

38from django.utils.encoding import force_str 

39from django.utils.translation import gettext 

40 

41log = logging.getLogger(__name__) 

42 

43 

44# ============================================================================= 

45# Action-restricted ModelAdmin classes 

46# ============================================================================= 

47 

48 

49class ReadOnlyChangeList(ChangeList): 

50 """ 

51 Variant of :class:`django.contrib.admin.views.main.ChangeList` that that 

52 changes the text for a read-only context. 

53 """ 

54 

55 def __init__(self, *args, **kwargs) -> None: 

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

57 if self.is_popup: 

58 title = gettext("Select %s") 

59 else: 

60 title = gettext("Select %s to view") 

61 self.title = title % force_str(self.opts.verbose_name) 

62 

63 

64class ReadOnlyModelAdmin(ModelAdmin): 

65 """ 

66 ModelAdmin that allows users to view ("change"), but not add/edit/delete. 

67 

68 You also need to do this: 

69 

70 .. code-block:: none 

71 

72 my_admin_site.index_template = 'admin/viewchange_admin_index.html' 

73 

74 ... to give a modified admin/index.html that says "View/change" not 

75 "Change". 

76 

77 """ 

78 

79 # https://stackoverflow.com/questions/3068843/permission-to-view-but-not-to-change-django # noqa: E501 

80 # See also https://stackoverflow.com/questions/6680631/django-admin-separate-read-only-view-and-change-view # noqa: E501 

81 # django/contrib/admin/templates/admin/change_form.html 

82 # django/contrib/admin/templatetags/admin_modify.py 

83 # https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.ModelAdmin.change_view # noqa: E501 

84 

85 # Remove the tickbox for deletion, and the top/bottom action bars: 

86 actions = None 

87 

88 # When you drill down into a single object, use a custom template 

89 # that removes the 'save' buttons: 

90 change_form_template = "admin/readonly_view_form.html" 

91 

92 def has_add_permission(self, request: HttpRequest, obj=None) -> bool: 

93 # Don't let the user add objects. 

94 return False 

95 

96 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool: 

97 # Don't let the user delete objects. 

98 return False 

99 

100 # Don't remove has_change_permission, or you won't see anything. 

101 # def has_change_permission(self, request, obj=None): 

102 # return False 

103 

104 def save_model( 

105 self, request: HttpRequest, obj, form: ModelForm, change: bool 

106 ): 

107 # Return nothing to make sure user can't update any data 

108 pass 

109 

110 # Make list say "Select [model] to view" not "... change" 

111 def get_changelist( 

112 self, request: HttpRequest, **kwargs 

113 ) -> Type[ChangeList]: 

114 return ReadOnlyChangeList 

115 

116 # Make single object view say "View [model]", not "Change [model]" 

117 def change_view( 

118 self, 

119 request: HttpRequest, 

120 object_id: int, 

121 form_url: str = "", 

122 extra_context: Dict[str, Any] = None, 

123 ) -> HttpResponse: 

124 extra_context = extra_context or {} 

125 # noinspection PyProtectedMember 

126 extra_context["title"] = "View %s" % force_str( 

127 self.model._meta.verbose_name 

128 ) 

129 return super().change_view( 

130 request, object_id, form_url, extra_context=extra_context 

131 ) 

132 

133 

134class AddOnlyModelAdmin(ModelAdmin): 

135 """ 

136 ModelAdmin that allows add, but not edit or delete. 

137 

138 Optional extra class attribute: ``fields_for_viewing``. 

139 """ 

140 

141 actions = None 

142 

143 # When you drill down into a single object, use a custom template 

144 # that removes the 'save' buttons: 

145 change_form_template = "admin/readonly_view_form.html" 

146 

147 # But keep the default for adding: 

148 add_form_template = "admin/change_form.html" 

149 

150 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool: 

151 return False 

152 

153 def get_changelist( 

154 self, request: HttpRequest, **kwargs 

155 ) -> Type[ChangeList]: 

156 return ReadOnlyChangeList 

157 

158 # This is an add-but-not-edit class. 

159 # https://stackoverflow.com/questions/7860612/django-admin-make-field-editable-in-add-but-not-edit # noqa: E501 

160 def get_readonly_fields(self, request: HttpRequest, obj=None) -> List[str]: 

161 if obj: # obj is not None, so this is an edit 

162 # self.__class__ is the derived class 

163 if hasattr(self.__class__, "fields_for_viewing"): 

164 # noinspection PyUnresolvedReferences,PyTypeChecker 

165 return self.__class__.fields_for_viewing 

166 elif hasattr(self.__class__, "readonly_fields"): 

167 return self.__class__.readonly_fields 

168 else: 

169 return self.__class__.fields 

170 else: # This is an addition 

171 return [] 

172 

173 def get_fields(self, request: HttpRequest, obj=None) -> List[str]: 

174 if obj: # edit (view) 

175 if hasattr(self.__class__, "fields_for_viewing"): 

176 # noinspection PyUnresolvedReferences,PyTypeChecker 

177 return self.__class__.fields_for_viewing 

178 return self.__class__.fields 

179 

180 # Make single object view say "View [model]", not "Change [model]" 

181 def change_view( 

182 self, 

183 request: HttpRequest, 

184 object_id: int, 

185 form_url: str = "", 

186 extra_context: Dict[str, Any] = None, 

187 ) -> HttpResponse: 

188 extra_context = extra_context or {} 

189 # noinspection PyProtectedMember 

190 extra_context["title"] = "View %s" % force_str( 

191 self.model._meta.verbose_name 

192 ) 

193 return super().change_view( 

194 request, object_id, form_url, extra_context=extra_context 

195 ) 

196 

197 

198class EditOnlyModelAdmin(ModelAdmin): 

199 """ 

200 ModelAdmin that allows editing, but not add or delete. 

201 

202 Designed for e.g. when you have a fixed set of PKs. In that situation, 

203 ensure the PK field is in ``readonly_fields``. 

204 """ 

205 

206 actions = None 

207 

208 def has_add_permission(self, request: HttpRequest, obj=None) -> bool: 

209 return False 

210 

211 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool: 

212 return False 

213 

214 

215class EditOnceOnlyModelAdmin(ModelAdmin): 

216 """ 

217 ModelAdmin that allows editing, but not add or delete. 

218 

219 Designed for e.g. when you have a fixed set of PKs. In that situation, 

220 ensure the PK field is in ``readonly_fields``. 

221 """ 

222 

223 actions = None 

224 

225 change_form_template = "admin/edit_once_view_form.html" 

226 

227 def has_add_permission(self, request: HttpRequest, obj=None) -> bool: 

228 return False 

229 

230 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool: 

231 return False 

232 

233 

234class AllStaffReadOnlyModelAdmin(ReadOnlyModelAdmin): 

235 """ 

236 ReadOnlyModelAdmin that allows access to all staff, not just superusers. 

237 (No easy way to make this work via multiple inheritance.) 

238 """ 

239 

240 def has_module_permission(self, request: HttpRequest) -> bool: 

241 return request.user.is_staff 

242 

243 def has_change_permission(self, request: HttpRequest, obj=None) -> bool: 

244 return request.user.is_staff