"""
crate_anon/crateweb/extra/admin.py
===============================================================================
Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CRATE.
CRATE is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CRATE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CRATE. If not, see <https://www.gnu.org/licenses/>.
===============================================================================
**Extensions to Django admin site classes.**
"""
import logging
from typing import Any, Dict, List, Type
from django.contrib.admin import ModelAdmin
from django.contrib.admin.views.main import ChangeList
from django.forms import ModelForm
from django.http import HttpResponse
from django.http.request import HttpRequest
from django.utils.encoding import force_str
from django.utils.translation import gettext
log = logging.getLogger(__name__)
# =============================================================================
# Action-restricted ModelAdmin classes
# =============================================================================
[docs]class ReadOnlyChangeList(ChangeList):
"""
Variant of :class:`django.contrib.admin.views.main.ChangeList` that that
changes the text for a read-only context.
"""
[docs] def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if self.is_popup:
title = gettext("Select %s")
else:
title = gettext("Select %s to view")
self.title = title % force_str(self.opts.verbose_name)
[docs]class ReadOnlyModelAdmin(ModelAdmin):
"""
ModelAdmin that allows users to view ("change"), but not add/edit/delete.
You also need to do this:
.. code-block:: none
my_admin_site.index_template = 'admin/viewchange_admin_index.html'
... to give a modified admin/index.html that says "View/change" not
"Change".
"""
# https://stackoverflow.com/questions/3068843/permission-to-view-but-not-to-change-django # noqa
# See also https://stackoverflow.com/questions/6680631/django-admin-separate-read-only-view-and-change-view # noqa
# django/contrib/admin/templates/admin/change_form.html
# django/contrib/admin/templatetags/admin_modify.py
# https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.ModelAdmin.change_view # noqa
# Remove the tickbox for deletion, and the top/bottom action bars:
actions = None
# When you drill down into a single object, use a custom template
# that removes the 'save' buttons:
change_form_template = "admin/readonly_view_form.html"
[docs] def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
# Don't let the user add objects.
return False
[docs] def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
# Don't let the user delete objects.
return False
# Don't remove has_change_permission, or you won't see anything.
# def has_change_permission(self, request, obj=None):
# return False
[docs] def save_model(
self, request: HttpRequest, obj, form: ModelForm, change: bool
):
# Return nothing to make sure user can't update any data
pass
# Make list say "Select [model] to view" not "... change"
[docs] def get_changelist(
self, request: HttpRequest, **kwargs
) -> Type[ChangeList]:
return ReadOnlyChangeList
# Make single object view say "View [model]", not "Change [model]"
def change_view(
self,
request: HttpRequest,
object_id: int,
form_url: str = "",
extra_context: Dict[str, Any] = None,
) -> HttpResponse:
extra_context = extra_context or {}
# noinspection PyProtectedMember
extra_context["title"] = "View %s" % force_str(
self.model._meta.verbose_name
)
return super().change_view(
request, object_id, form_url, extra_context=extra_context
)
[docs]class AddOnlyModelAdmin(ModelAdmin):
"""
ModelAdmin that allows add, but not edit or delete.
Optional extra class attribute: ``fields_for_viewing``.
"""
actions = None
# When you drill down into a single object, use a custom template
# that removes the 'save' buttons:
change_form_template = "admin/readonly_view_form.html"
# But keep the default for adding:
add_form_template = "admin/change_form.html"
[docs] def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
return False
[docs] def get_changelist(
self, request: HttpRequest, **kwargs
) -> Type[ChangeList]:
return ReadOnlyChangeList
# This is an add-but-not-edit class.
# https://stackoverflow.com/questions/7860612/django-admin-make-field-editable-in-add-but-not-edit # noqa
[docs] def get_readonly_fields(self, request: HttpRequest, obj=None) -> List[str]:
if obj: # obj is not None, so this is an edit
# self.__class__ is the derived class
if hasattr(self.__class__, "fields_for_viewing"):
# noinspection PyUnresolvedReferences,PyTypeChecker
return self.__class__.fields_for_viewing
elif hasattr(self.__class__, "readonly_fields"):
return self.__class__.readonly_fields
else:
return self.__class__.fields
else: # This is an addition
return []
[docs] def get_fields(self, request: HttpRequest, obj=None) -> List[str]:
if obj: # edit (view)
if hasattr(self.__class__, "fields_for_viewing"):
# noinspection PyUnresolvedReferences,PyTypeChecker
return self.__class__.fields_for_viewing
return self.__class__.fields
# Make single object view say "View [model]", not "Change [model]"
def change_view(
self,
request: HttpRequest,
object_id: int,
form_url: str = "",
extra_context: Dict[str, Any] = None,
) -> HttpResponse:
extra_context = extra_context or {}
# noinspection PyProtectedMember
extra_context["title"] = "View %s" % force_str(
self.model._meta.verbose_name
)
return super().change_view(
request, object_id, form_url, extra_context=extra_context
)
[docs]class EditOnlyModelAdmin(ModelAdmin):
"""
ModelAdmin that allows editing, but not add or delete.
Designed for e.g. when you have a fixed set of PKs. In that situation,
ensure the PK field is in ``readonly_fields``.
"""
actions = None
[docs] def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
return False
[docs] def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
return False
[docs]class EditOnceOnlyModelAdmin(ModelAdmin):
"""
ModelAdmin that allows editing, but not add or delete.
Designed for e.g. when you have a fixed set of PKs. In that situation,
ensure the PK field is in ``readonly_fields``.
"""
actions = None
change_form_template = "admin/edit_once_view_form.html"
[docs] def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
return False
[docs] def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
return False
[docs]class AllStaffReadOnlyModelAdmin(ReadOnlyModelAdmin):
"""
ReadOnlyModelAdmin that allows access to all staff, not just superusers.
(No easy way to make this work via multiple inheritance.)
"""
[docs] def has_module_permission(self, request: HttpRequest) -> bool:
return request.user.is_staff
[docs] def has_change_permission(self, request: HttpRequest, obj=None) -> bool:
return request.user.is_staff