25

I have a Django Model which I wish to be only readonly. No adds and edits allowed.

I have marked all fields readonly and overridden has_add_permission in ModelAdmin as:

class SomeModelAdmin(admin.ModelAdmin):
     def has_add_permission(self, request):
        return False

Is there a similar has_edit_permission? Which can be disabled to remove "Save" and "Save and continue" buttons? And replace by a simple "Close and Return" button.

Django Documentation Only mentions only about read only fields not about overriding edit permissions.

Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
jerrymouse
  • 16,964
  • 16
  • 76
  • 97

10 Answers10

31

For Django 1.11:

def has_add_permission(self, request, obj=None):
    return False

def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
    extra_context = extra_context or {}
    extra_context['show_save_and_continue'] = False
    extra_context['show_save'] = False
    return super(YourModelAdmin, self).changeform_view(request, object_id, extra_context=extra_context)
xleon
  • 6,201
  • 3
  • 36
  • 52
  • Thanks! Worked a treat in combination with @mat_gessel's answer (I created a template in which `read_only` hides the save buttons), but on it's own, I'm not able to hide the 'Save and add another' button in Django 1.9 without overriding the template... – ptim Jul 15 '17 at 19:00
  • 1
    @ptim: https://stackoverflow.com/a/54202294 explains how to set `show_save_and_add_another=False` in Django < 2.1. For Django>=2.1 it is easier, it think. – djvg Jan 15 '19 at 15:58
17

The easiest method would be disabling respective permissions in ModelAdmin class. For example, I have a Cart model that I want an admin to only view (list and details) and all I did was to add the following functions to CartAdmin class to disable delete, change and add

class CartAdmin(admin.ModelAdmin):
    list_display = ['listing']

    def has_add_permission(self, request, obj=None):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False

The three methods has_add_permission, has_change_permission and has_delete_permission are the ones that disable add button, add form, edit form and delete buttons in the admin

Here is a sample output when viewing a record details in the admin that has the above permissions disabled

enter image description here

As you can see the diagram above, you only have close button and the details are not displayed in a form

Christopher Kikoti
  • 2,487
  • 1
  • 24
  • 29
16

I had same problem. I fixed it in admin.py

from django.contrib.admin.templatetags.admin_modify import register, submit_row as original_submit_row

@register.inclusion_tag('admin/submit_line.html', takes_context=True)
def submit_row(context):
''' submit buttons context change '''
ctx = original_submit_row(context)
ctx.update({
    'show_save_and_add_another': context.get('show_save_and_add_another',
                                             ctx['show_save_and_add_another']),
    'show_save_and_continue': context.get('show_save_and_continue',
                                          ctx['show_save_and_continue']),
    'show_save': context.get('show_save',
                             ctx['show_save']),
    'show_delete_link': context.get('show_delete_link', ctx['show_delete_link'])
})
return ctx

In MyModelAdmin class, add following function

@classmethod
def has_add_permission(cls, request):
    ''' remove add and save and add another button '''
    return False

def change_view(self, request, object_id, extra_context=None):
    ''' customize add/edit form '''
    extra_context = extra_context or {}
    extra_context['show_save_and_continue'] = False
    extra_context['show_save'] = False
    return super(MyModelAdmin, self).change_view(request, object_id, extra_context=extra_context)
xleon
  • 6,201
  • 3
  • 36
  • 52
J.Preet
  • 159
  • 1
  • 2
14

Override the templates/admin/submit_line.html template and make the buttons whatever you want. You can do this for only the specific model by putting it in templates/admin/[app_label]/[model]/submit_line.html.

To conditionally show the default submit line or your custom submit line, override ModelAdmin.change_view, and add a boolean to extra_context:

class MyModelAdmin(admin.ModelAdmin):
    ...
    def change_view(self, request, object_id, extra_context=None):
        if not request.user.is_superuser:
            extra_context = extra_context or {}
            extra_context['readonly'] = True

        return super(MyModelAdmin, self).change_view(request, object_id, extra_context=extra_context)
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 1
    Is overriding `templates/admin/[app_label]/[model]/submit_line.html` still supposed to work with Django 1.4? According to the [docs](https://docs.djangoproject.com/en/dev/ref/contrib/admin/#templates-which-may-be-overridden-per-app-or-model) only certain templates can be overriden on a per app/model basis. – Dirk Eschler Aug 14 '12 at 12:39
  • Perhaps not. However, both the `add_view` and `change_view` views add the `app_label` to the context, so you could use that to test for a particular app. Varying on the model will require overriding those two views on your `ModelAdmin` and adding `module_name` to the context as well. – Chris Pratt Aug 14 '12 at 14:31
  • 3
    @ChrisPratt `extra_context['readonly'] = True` doesn't work with Django 1.4. Somehow the variable never makes it into `submit_line.html` – memyself Sep 06 '12 at 09:06
  • It still works. The only change Django made in 1.4 was the add the `form_url` argument to the method definition. As long as you pass `extra_context` as a kwarg, as I did, it won't affect anything. However, if you just pass it as an argument, i.e. `change_view(request, object_id, extra_context)`, it gets stuffed into the `form_url` arg instead of `extra_context` where it belongs. – Chris Pratt Sep 06 '12 at 14:45
  • 2
    It's too bad Django 1.4 no longer lets `submit_line.html` be overridden on a per app/model basis. (Wonder why they got rid of that?) Anyhow, in `submit_line.html`, whether the individual buttons are shown is determined by different context bools, e.g. `show_save`, `show_delete_link` etc. How can I set these to hide/show buttons without actually overriding the template? I've tried `extra_context` but that doesn't work -- it seems that's only for, well, *extra* context, not changing existing context variables. Any thoughts? – Ghopper21 Sep 13 '12 at 06:57
  • These variables are defined in the `submit_row` template tag at `django/contrib/admin/templatetags/admin_modify.py`. You can override it creating your own template tag with the same name. – Anatoly Scherbakov Feb 02 '14 at 11:45
  • 1
    You can only override certain templates on a per app basis. submit_line.html is NOT one of these. – Mark Mar 13 '14 at 11:30
  • I don't think it works for a specific model admin, its a feature that wasn't added yet? https://code.djangoproject.com/ticket/11974 It doesn't work for me in django 1.6.7 – radtek Oct 09 '14 at 18:37
  • @Mark "You can only override certain templates on a per app basis. submit_line.html is NOT one of these." yeah that seems to be the case but doc link? – spinkus Sep 14 '16 at 10:08
  • @Mark never mind - https://docs.djangoproject.com/en/dev/ref/contrib/admin/#templates-which-may-be-overridden-per-app-or-model – spinkus Sep 14 '16 at 10:09
10

Updated answer using Django 1.8 (Python 3 syntax).

There are three things to do:
1) extend the admin change form template, adding an if to conditionally suppress the submit buttons
2) override admin.ModelAdmin.change_view() and set a context var for the template if to read
3) prohibit unwanted POST requests (from DOM hacking, curl/Postman)


MyProject/my_app/templates/admin/my_app/change_form.html

{% extends "admin/change_form.html" %}
{% load admin_modify %}
{% block submit_buttons_top %}{% if my_editable %}{% submit_row %}{% endif %}{% endblock %}
{% block submit_buttons_bottom %}{% if my_editable %}{% submit_row %}{% endif %}{% endblock %}

MyProject/my_app/admin.py (MyModelAdmin)

def change_view(self, request, object_id, form_url='', extra_context=None):
  obj = MyModel.objects.get(pk=object_id)
  editable = obj.get_status() == 'Active'

  if not editable and request.method == 'POST':
    return HttpResponseForbidden("Cannot change an inactive MyModel")

  more_context = {
    # set a context var telling our customized template to suppress the Save button group
    'my_editable': editable,
  }
  more_context.update(extra_context or {})
  return super().change_view(request, object_id, form_url, more_context)
Mat Gessel
  • 562
  • 8
  • 17
  • 3
    This is absolutely the best existing answer on this issue. Why so few upvotes? Also, if your template on custom path, use `template` property inside `MyModelAdmin` class. – Damaged Organic Dec 05 '16 at 12:25
  • By default Django will check a number of locations for the change_form_template `admin/myapp/model/change_form.html` and `admin/myapp/change_form.html`. – Danielle Madeley Mar 22 '17 at 05:34
  • How do I disable it for all apps? (Following this, I can disable for individual apps) – akashrajkn May 02 '17 at 06:41
  • 1
    @akashrajkn you could subclass `ModelAdmin` as `ReadOnlyModelAdmin` and use it for all the apps you define. Maybe cleanest to define `template` on that subclass as `readonly_change_form.html`? – ptim Jul 15 '17 at 18:22
  • 1
    In addition, suggest defining `get_readonly_fields` so that fields are displayed as text, rather than form fields; see https://stackoverflow.com/a/7864099/2586761 – ptim Jul 15 '17 at 18:53
5

I had the same problem - the easiest way to do this, is to include some custom JS.

In you admin.py file include

class Media:
    js = ('/static/js/admin.js',)

Then in your admin.js file, include the following JS.

(function($) {
    $(document).ready(function($) {
         $(".submit-row").hide()
    });
})(django.jQuery);

The row is gone - it should work in all versions of Django too.

Mark
  • 2,522
  • 5
  • 36
  • 42
  • `has_delete_permission` removes delete action even list page. Overriding `change_view` does not work. Overriding `submit_row` template tag also changes buttons over all models. – pincoin Apr 15 '18 at 14:29
4

This has been possible for a while. The names are has_add_permission, has_change_permission and has_delete_permission. See the django admin documentation for reference. Here is also an example:

@admin.register(Object)
class Admin(admin.ModelAdmin):

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False
shezi
  • 560
  • 3
  • 18
1

Aug, 2022 Update:

You can remove "SAVE" button, "Save and continue editing" button, "Save and add another" button and "Delete" button from a specific admin.

For example, this is "Person" model in "store" app below:

# "store/models.py"

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

    def __str__(self):
        return self.first_name + " " + self.last_name

    class Meta:
        verbose_name = "Person"
        verbose_name_plural = "Person"

Then, this is "Person" admin in "store" app below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    pass

Then, this is how "Add person" page looks like:

enter image description here

Then, this is how "Change person" page looks like:

enter image description here

Then, this is how "Select person to change" page looks like:

enter image description here

Then, this is how "Person" admin on "Store administration" page looks like:

enter image description here

First, to remove "SAVE" button, set "False" to "extra_context['show_save']" in "changeform_view()" as shown below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = extra_context or {}

        extra_context['show_save'] = False # Here

        return super().changeform_view(request, object_id, form_url, extra_context)

Then, "SAVE" button is removed from "Add person" page and "Change person" page. *Actually, "SAVE" button is replaced with "Close" buttom as shown below:

enter image description here

enter image description here

Next, to remove "Save and continue editing" button, set "False" to "extra_context['show_save_and_continue']" in "changeform_view()" as shown below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = extra_context or {}

        extra_context['show_save'] = False
        extra_context['show_save_and_continue'] = False # Here

        return super().changeform_view(request, object_id, form_url, extra_context)

Then, "Save and continue editing" button is removed from "Add person" page and "Change person" page as shown below:

enter image description here

enter image description here

Next, to remove "Save and add another" button, return "False" in "has_add_permission()" as shown below. *After this, "Add person" page can no longer be accessed:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = extra_context or {}

        extra_context['show_save'] = False
        extra_context['show_save_and_continue'] = False

        return super().changeform_view(request, object_id, form_url, extra_context)

    def has_add_permission(self, request, obj=None): # Here
        return False

Then, "Save and add another" button is removed from "Change person" page as shown below:

enter image description here

Then, "ADD PERSON" button is also removed from "Select person to change" page as shown below:

enter image description here

Then, "➕ADD" button is also removed from "Person" admin on "Store administration" page as shown below:

enter image description here

Next, to remove "Delete" button, set "False" to "extra_context['show_delete']" in "changeform_view()" as shown below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = extra_context or {}

        extra_context['show_save'] = False
        extra_context['show_save_and_continue'] = False
        extra_context['show_delete'] = False # Here

        return super().changeform_view(request, object_id, form_url, extra_context)

    def has_add_permission(self, request, obj=None):
        return False

Then, "Delete" button is removed from "Change person" page as shown below:

enter image description here

Actually, you can also remove "Delete" button by returning "False" in "has_delete_permission()" as shown below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = extra_context or {}

        extra_context['show_save'] = False
        extra_context['show_save_and_continue'] = False
        # extra_context['show_delete'] = False

        return super().changeform_view(request, object_id, form_url, extra_context)

    def has_add_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None): # Here
        return False

Then, "Delete" button is removed from "Change person" page as shown below:

enter image description here

Then, "Action" select dropdown box is also removed from "Select person to change" page as shown below:

enter image description here

In addition, you can make the fields on "Change person" page unchangeable by returning "False" in "has_change_permission()" as shown below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = extra_context or {}

        extra_context['show_save'] = False
        extra_context['show_save_and_continue'] = False
        # extra_context['show_delete'] = False

        return super().changeform_view(request, object_id, form_url, extra_context)

    def has_add_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None): 
        return False

    def has_change_permission(self, request, obj=None): # Here
        return False

Then, the fields on "Change person" page are made unchangeable as shown below:

enter image description here

Then, "✏️Change" button is replaced with "️View" for "Person" admin on "Store administration" page as shown below:

enter image description here

In addition, you can remove "Person" admin from "Store administration" page by returning "False" in "has_view_permission()" as shown below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = extra_context or {}

        extra_context['show_save'] = False
        extra_context['show_save_and_continue'] = False
        # extra_context['show_delete'] = False

        return super().changeform_view(request, object_id, form_url, extra_context)

    def has_add_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None): 
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_view_permission(self, request, obj=None): # Here
        return False

Then, "Person" admin is removed from "Store administration" page as shown below:

enter image description here

Finally, you can replace "changeform_view()" with "render_change_form()" which can also remove "SAVE" button, "Save and continue editing" button and "Delete" button with "context.update()" as shown below:

# "store/admin.py"

from django.contrib import admin
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    
    # Here
    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
        context.update({
            'show_save': False, # Here
            'show_save_and_continue': False, # Here
            # 'show_delete': False, # Here
        })
        return super().render_change_form(request, context, add, change, form_url, obj)

    def has_add_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None): 
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_view_permission(self, request, obj=None):
        return False
Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
0

You could try this package Django Admin View Permission. This package adds a view permission for the specified models and handles the other stuff automatically.

lefterisnik
  • 137
  • 1
  • 3
0

Based on the excellent answer from @mat_gessel, here's my solution:

The main differences are UX'y:

Also:

  • override change_form.html app-wide, because read_only is such a useful, non-invasive enhancement
  • define has_delete_permission (may not be required by the OP)
  • test request.method != 'GET' to prevent PATCH and friends (not altogether sure if this is required, tbh)

my_app/admin.py

from django.core.urlresolvers import reverse
from django.shortcuts import redirect

from django.contrib import admin
from django.contrib import messages


class MyModelAdmin(admin.ModelAdmin):
    # let's assume two fields...
    fields = (field1, field2)

    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        if object_id:
            extra_context = extra_context or {}
            extra_context['read_only'] = True

            if request.method != 'GET':
                messages.error(request, "Cannot edit a MyModel object")
                return redirect(
                    reverse('admin:myapp_mymodel_change', args=[object_id])
                )

        return super(MyModelAdmin, self).changeform_view(request, object_id, extra_context=extra_context)

    def has_delete_permission(self, request, obj=None):
        return False

    def get_readonly_fields(self, request, obj=None):
        if obj:
            # display all fields as text, rather than inputs
            return (field1, field2)
        else:
            return []

admin/change_form.html

{% extends "admin/change_form.html" %}
{% load admin_modify %}
{# remove the save buttons if read_only is truthy #}
{% block submit_buttons_top %}{% if not read_only %}{% submit_row %}{% endif %}{% endblock %}
{% block submit_buttons_bottom %}{% if not read_only %}{% submit_row %}{% endif %}{% endblock %}

(Tested on Django 1.9: heads up: some imports have moved since then, eg reverse)

ptim
  • 14,902
  • 10
  • 83
  • 103