Превращение @property.setter в редактируемое поле в Django Admin Inlines

Документирую свое решение здесь, возможно, у кого-то возникнет идея получше.

В моем текущем проекте у нас есть вычисляемое @свойство, отображающее значение на основе критериев из других полей. До этого момента это было поле, доступное только для чтения, поэтому не было никаких проблем с обычным интерфейсом администрирования django. Недавно клиент захотел сделать это поле редактируемым (в моем реальном проекте оно отображает DateTime); однако значения были взяты из двух разных мест, основанных на типе данных и ограничениях БД. Моей первой мыслью было поместить оба поля на форму и внедрить JS для отображения/скрытия соответствующих полей, но я хотел посмотреть, можно ли это сделать с помощью django.

После некоторых ответов, которые я нашел здесь и здесь, я решил пойти с @property.setter и попытаться заставить его работать с сайтом Admin. Дополнительный контекст заключается в том, что эта форма в настоящее время отображается в Inline. Я не уверен, была ли это проблема версии или потому, что я делаю это для инлайна, но def __init__() из решения по ссылке для заполнения формы не работал для меня.

Строчные формы ...

Вот мое решение, как сделать @property редактируемым на сайте DjangoAdmin, что по сути дает вам возможность геттеров и сеттеров в django admin.

Основная проблема заключается в том, что @properties не отображаются как поля в ModelAdmin. При вводе имени свойства в fields=[] возникает ошибка "FieldError: unknown fields", а при вводе имени свойства в readonly_fields=[] невозможно его редактировать. Решение состоит в том, чтобы создать пользовательскую форму с дополнительным полем, а затем указать форме, что делать с этим полем.

Вот пример моих моделей, которые имеют связанные поля друг с другом. Помните, что все это было сделано для Inline, хотя, казалось бы, это должно прекрасно работать для любой другой формы.

class Model(models.Model):
    name = models.CharField(max_length=200)
    stuff = models.CharField(max_length=200)

class RelatedModel(models.Model):
    related_field = models.ForeignKey(Model, on_delete=models.CASCADE)

    conditional_field = models.CharField(max_length=5,choices=[("left", "left"), ("right","right")],blank=False)

    value_in_left_field = models.CharField(max_length=200, null=True)
    value_in_right_field = models.CharField(max_length=200, null=True)

      # the property creates the value based on a condition
    @property
    def current_value(self):
        return self.value_in_right_field if self.conditional_field == "right" else self.value_in_left_field

      # the setter, sets the value based on a condition
    @current_value.setter
    def current_value(self, new_value):
          # provide whatever logic you want for your setter here
        if self.conditional_field == "left":
            self.value_in_left_field = new_value
            self.value_in_right_field = None
        else:
            self.value_in_right_field = new_value
            self.value_in_left_field = None

      # this line lets you change the short description on a @property
    current_value.fget.short_description = "a different label"

и вот классы администратора, чтобы сделать поле редактируемым, есть 3 части, чтобы заставить это работать:

  1. добавьте дополнительное поле в пользовательскую форму
  2. получить начальное значение для поля в форме
  3. указать форме, что делать с дополнительным полем
from django.contrib import admin
from django.forms import ModelForm, CharField
from .models import Model, RelatedModel

class RelatedModelInlineForm(ModelForm):
    class Meta:
        exclude = []
        model = RelatedModel

      # 1. add the field to the form
    current_value = CharField(max_length=200)

      # 2. set the initial value for the field
    def get_initial_for_field(self, field,field_name):
        if field_name == "current_value":
            return self.instance.current_value
        return super().get_initial_for_field(field, field_name)

      # 3. tell the form what to do with the extra field
    def save(self, commit=True):
        model = super().save(commit=False)
        model.current_value = self.cleaned_data["current_value"]
        if commit:
            model.save()
        return model


class RelatedModelInline(admin.TabularInline):
    model = RelatedModel
    extra = 0
    form = RelatedModelInlineForm
    fields = [
        "conditional_field", 
        "current_value",
    ]


@admin.register(Model)
class ModelAdmin(admin.ModelAdmin):
    model = Model
    inlines = [RelatedModelInline]

Теперь, в процессе работы, мне пришло в голову, что можно просто поместить логику сеттера в метод сохранения, что на самом деле просто делает его встроенным в пользовательскую форму. лол, но предварительное заполнение поля данными модели - это довольно круто. ааааа, и .setter держит логику близко к модели, а не близко к форме, так что выбирайте по своему вкусу. Я не могу сказать, что лучше.

    def save(self, commit=True):
        model = super().save(commit=False)
        
     # logic for saving the field the way you want 
        if model.conditional_field == "left":
            model.value_in_left_field = self.cleaned_data["current_value"]
            model.value_in_right_field = None
        else:
            model.value_in_right_field = self.cleaned_data["current_value"]
            model.value_in_left_field = None

        if commit:
            model.save()
        return model

В конечном счете, я думаю, если вы делаете это вычисляемое поле специально для страницы администратора, метод save(), вероятно, более читабелен, но если эта функциональность вам нужна во фронтенде и бэкенде, тогда поместить ее в .setter будет лучшим подходом?

Как видите, значение из поля формы сохраняется в нужное место в зависимости от условия. Интерфейс администратора с инлайн-формой

Вернуться на верх