21

I have a number of models with a ForeignKey relationship to a Person model. For example:

class PersonData(BaseModel):
    person = models.ForeignKey(Person)
    data = models.TextField()

I want to lock down the admin such that once a PersonData object is created, an admin user can change the data, but can't change the Person.

At first it seemed pretty straightforward -- I put this in the PersonDataAdmin class:

def get_readonly_fields(self, request, obj=None):
    if obj:
        return self.readonly_fields + ('person',)
    return self.readonly_fields

On the display side, that worked as expected -- I see the value for person, but it's grayed out so I can't change it -- but then when I try to change the data and submit the form, I get an error message, "Please correct the error below." No other message appears, but with a bit of digging, I discovered that the form is missing a value for the required person field.

I've investigated a solution that would involve creating a custom form that would disable this field selectively (something like this or this), but (a) I wasn't successful in getting it to work, and (b) it seemed like a ton of code for what seems like a much simpler situation. I also looked into using exclude, but ran into the same problem as read_only.

How can I accomplish this?

halfer
  • 19,824
  • 17
  • 99
  • 186
TAH
  • 1,658
  • 1
  • 19
  • 37

7 Answers7

32

What I usually do is set the field in the model as editable=False and then in the admin.py set the field as read-only like this,

class PersonData(BaseModel):
    person = models.ForeignKey(Person, editable=False)
    data = models.TextField()

Then in admin.py

class PersonDataAdmin(admin.ModelAdmin):
    readonly_fields=('person',)

Hope this works!

manuhortet
  • 479
  • 7
  • 20
devdob
  • 1,404
  • 12
  • 13
  • That doesn't work in this case, since the field is editable half the time. – TAH Jan 06 '18 at 22:35
  • 3
    settings a model field as `editable=False` won't show it on the Admin and skip validation. The read_only field does the job. Ref: https://docs.djangoproject.com/en/3.1/ref/models/fields/#editable – wagnerdelima Sep 22 '20 at 13:20
4

I hit the same problem. A workaround is to define a method in the Admin class that returns the value of the readonly field that you want to display. Then use the name of the method where you would previously have used the name of the field. e.g. rather than:

class PersonDataAdmin(admin.ModelAdmin):
    readonly_fields=('person',)

use:

class PersonDataAdmin(admin.ModelAdmin):
    readonly_fields=('person_method',)

    def person_method(self, obj):
        return obj.person

    person_method.short_description = 'Person'

The short_description sets the label for the field. If you don't set it then the field will be labelled as "Person method"

Presumably in the OPs case get_readonly_fields becomes:

def get_readonly_fields(self, request, obj=None):
    if obj:
        return self.readonly_fields + ('person_method',)
    return self.readonly_fields
Tim Morley
  • 51
  • 4
  • Using only `get_readonly_fields` from this answer does the trick on non-method-sourced (normal) field. Not sure how the whole answer would work - method-sourced fields are read only anyway. – Ctrl-C Jul 31 '19 at 10:32
3

You could extend ModelAdmin.get_form() and use Field.disabled to disable the person field, as follows:

def get_form(self, request, obj=None, change=False, **kwargs):
    form = super().get_form(request, obj, change, **kwargs)
    if obj:
        form.base_fields['person'].disabled = True
    return form

This will prevent the person field from being modified.

However, it will still be styled as a select box, and it may still have the "change" and "add another" buttons next to it.

These buttons are added by the RelatedFieldWidgetWrapper. One way to remove them is by removing the wrapper:

...
field = form.base_fields['person']
field.disabled = True
if isinstance(field.widget, RelatedFieldWidgetWrapper):
    # unwrap the widget
    field.widget = field.widget.widget

Another way is to set the following widget attributes:

...
if isinstance(field.widget, RelatedFieldWidgetWrapper):
    field.widget.can_add_related = False
    field.widget.can_change_related = False

If you want the field to be styled as a readonly field, you'll need some extra work. Also see the fieldset.html template and AdminReadonlyField to understand how django admin displays foreign-key readonly_fields.

Another approach altogether would be to implement your own ReadOnlyWidget, similar to this, and just set form.base_fields['person'].widget = ReadOnlyWidget()

djvg
  • 11,722
  • 5
  • 72
  • 103
2

field1 is a required field here and it is made as read only in admin screen

model.py

class ModelName(models.Model):
  field1 = models.ForeignKey(
      ModelA, 
      on_delete=models.CASCADE,
      db_column='col1',
      related_name='col1',
      verbose_name='col1'
  )
  field2 = models.ForeignKey(
      ModelB, 
      on_delete=models.CASCADE,
      db_column='col2',
      related_name='col2',
      verbose_name='col2'
  )

admin.py

    @admin.register(ModelName)
    class AdminClassName(admin.ModelAdmin):
        fields = ('field1', 'field2')
        readonly_fields = ('field1')
1

Here's ModelAdmin.readonly_fields documentation.

Pilar Figueroa
  • 85
  • 1
  • 11
0

I had the exact same problem of yours. Check if you are using get_form on your admin class. Somehow this was the cause of my problem.

Alan
  • 378
  • 2
  • 8
0

In Django - 4.1.3

from django.contrib import admin
from app.models import User

class UserAdmin(admin.ModelAdmin):
   readonly_fields = ['last_login']

admin.site.register(User, UserAdmin)

If you want to make fields read-only for specific users

Tintin
  • 51
  • 6