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
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-27 10:34 -0500
1"""
2crate_anon/crateweb/extra/admin.py
4===============================================================================
6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CRATE.
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.
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.
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/>.
24===============================================================================
26**Extensions to Django admin site classes.**
28"""
30import logging
31from typing import Any, Dict, List, Type
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
41log = logging.getLogger(__name__)
44# =============================================================================
45# Action-restricted ModelAdmin classes
46# =============================================================================
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 """
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)
64class ReadOnlyModelAdmin(ModelAdmin):
65 """
66 ModelAdmin that allows users to view ("change"), but not add/edit/delete.
68 You also need to do this:
70 .. code-block:: none
72 my_admin_site.index_template = 'admin/viewchange_admin_index.html'
74 ... to give a modified admin/index.html that says "View/change" not
75 "Change".
77 """
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
85 # Remove the tickbox for deletion, and the top/bottom action bars:
86 actions = None
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"
92 def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
93 # Don't let the user add objects.
94 return False
96 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
97 # Don't let the user delete objects.
98 return False
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
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
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
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 )
134class AddOnlyModelAdmin(ModelAdmin):
135 """
136 ModelAdmin that allows add, but not edit or delete.
138 Optional extra class attribute: ``fields_for_viewing``.
139 """
141 actions = None
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"
147 # But keep the default for adding:
148 add_form_template = "admin/change_form.html"
150 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
151 return False
153 def get_changelist(
154 self, request: HttpRequest, **kwargs
155 ) -> Type[ChangeList]:
156 return ReadOnlyChangeList
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 []
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
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 )
198class EditOnlyModelAdmin(ModelAdmin):
199 """
200 ModelAdmin that allows editing, but not add or delete.
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 """
206 actions = None
208 def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
209 return False
211 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
212 return False
215class EditOnceOnlyModelAdmin(ModelAdmin):
216 """
217 ModelAdmin that allows editing, but not add or delete.
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 """
223 actions = None
225 change_form_template = "admin/edit_once_view_form.html"
227 def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
228 return False
230 def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
231 return False
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 """
240 def has_module_permission(self, request: HttpRequest) -> bool:
241 return request.user.is_staff
243 def has_change_permission(self, request: HttpRequest, obj=None) -> bool:
244 return request.user.is_staff