4

DRF will use the editable=False on a field to default the Serializer to read-only. This is a very helpful / safe default that I take advantage of (ie I won't forget to set the Serializer to read-only). That being said once I have set editable=False is there any way to then force the Django admin to allow editing one of those fields?

Presumably the admin is a super user and I do want him to be able to change the fields value but fore safety I want the default Serializer logic to be read only.

UPDATE

I don't actually need to be able to edit the field as much as "set-it" when I create the object.

Alex Rothberg
  • 10,243
  • 13
  • 60
  • 120
  • 2
    You are better off keeping `editable=True` and overriding `read_only=True` on the serializer level. DRF is much more flexible than the Django admin as far as this goes. – Kevin Brown-Silva Mar 18 '15 at 17:53
  • The issue that I was trying to avoid (and there may be no good way for this) was having to remember to set `read_only=True` on each Serializer for a given Model (for which there can be more than one). – Alex Rothberg Mar 18 '15 at 17:54
  • 1
    possible duplicate of [Django readonly field only on change, but not when creating](http://stackoverflow.com/questions/17613559/django-readonly-field-only-on-change-but-not-when-creating) –  Mar 19 '15 at 03:03

2 Answers2

5

You are going about this the wrong way.

Your models should be the most pure implementation of the things you are modelling. If something about a model is fixed (for example a creation date) it shouldn't be editable in the model, if its mutable, then leave as editable in the model.

Otherwise, in the future you (or someone else) might be stuck wondering why a field which is set to editable=False is some how being changed. Especially as the documentation states:

If False, the field will not be displayed in the admin or any other ModelForm. They are also skipped during model validation.

If you have one view in which it shouldn't be editable (such as in the API), then override it there.

If you have multiple serilaizers for a model, instead make an abstract serializer with a read_only_fields set and then subclass that. For example:

class AbstractFooSerializer(serializers.ModelSerializer):
    class Meta:
        model = Foo
        read_only_fields = ('bar',)


class MainFooSerializer(AbstractFooSerializer):
    pass

class DifferentFooSerializer(AbstractFooSerializer):
    pass

If you really, really want to use editable=False, but allow the item to be edited in the Admin site only on creation you have an up hill battle.

Probably the best approach would be to reimplement the AdminForm you are using for the Admin

So instead of:

class FooAdmin(admin.ModelAdmin):

Use:

class FooAdmin(admin.ModelAdmin):
    form = MySpecialForm

Then declare the form:

class MySpecialForm(forms.Model):
    def __init__(self, *args, **kwargs):
        self.is_new = False
        if kwargs.get('instance',None) is None:
            # There is no instance, thus its a new item
            self.is_new = True
            self.fields['one_time_field'] = forms.CharField() # Or what have you.
        super(MySpecialForm, self).__init__(*args, **kwargs)

    def save(self, commit=True):
         instance = super(MySpecialForm, self).save(commit)
         if self.is_new:
             instance.your_one_time_only_field = self.one_time_field
             instance.save()
         return instance

Note: you will need to manually add a field and save each readonly field that you want to do this for. This may or may not be 100% functional.

  • I was a little sloppy with language in my question and I have since updated my post. You make a fair point here: "in the future you (or someone else) might be stuck wondering why a field which is set to `editable=False` is some how being changed" but what about that case where while I don't want to edit the field I would like the ability to set it initially when I create the object from within the admin? – Alex Rothberg Mar 19 '15 at 02:51
  • Ah! In that case this is a duplicate. What you want is a `readonly` model, that can be set one in admin. I've marked this as a duplicate of this question which should do what you need. http://stackoverflow.com/questions/17613559/django-readonly-field-only-on-change-but-not-when-creating –  Mar 19 '15 at 03:04
  • Is there anyway that I can set `editable=False` and have this work? – Alex Rothberg Mar 19 '15 at 03:10
  • I tried your code to be allowed to set fields with editable=False in the admin view. It does not work. The biggest problem is that self.fields is not available before the constructor call. I didn't try it much further. – Sebastian Oct 26 '16 at 13:41
1

For those who want to allow editing of a non-editabled field only during creation (no instance.pk, yet):

# models.py
class Entity(Model):
    name = CharField(max_length=200, unique=True, null=False, blank=False, editable=False)

# admin.py
@register(Entity)
class EntityAdmin(ModelAdmin):
    def get_readonly_fields(self, request, obj=None):
        if obj:  # This is the case when obj is already created i.e. it's an edit
            return ['id', 'name']
        else:
            return []

    # this override prevents that the new_name field shows up in the change form if it's not a creation
    def get_form(self, request, obj=None, **kwargs):
        orig_self_form = self.form
        if not obj:
            self.form = CreateEntityForm
        result = super().get_form(request, obj=obj, **kwargs)
        self.form = orig_self_form
        return result

# forms.py
class CreateEntityForm(ModelForm):
    new_name = CharField(max_length=200, min_length=2, label='Name', required=True)

    def clean_new_name(self):
        code = self.cleaned_data['new_name']
        # validate uniqueness - if you need
        exists = Entity.objects.filter(name=code).first()
        if exists:
            raise ValidationError('Entity with this name already exists: {}', exists)
        return name

    def save(self, commit=True):
        if self.instance.pk:
            raise NotImplementedError('Editing of existing Entity is not allowed!')

        self.instance.name = self.cleaned_data['new_name'].upper()
        return super().save(commit)

    class Meta:
        model = Entity
        fields = ['new_name']
        exclude = ['id', 'name']
Risadinha
  • 16,058
  • 2
  • 88
  • 91