Восстановить пользовательское поле при редактировании формы администрирования Django

Вопрос или проблема

Учитывая текущие модели в models.py:

from django import forms
from django.db import models
from decimal import Decimal
from datetime import datetime, date

class Order (models.Model):
    assignment = models.ForeignKey("Assignment", on_delete=models.RESTRICT)
    qty = models.PositiveIntegerField(default=1)
    order_date = models.DateField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    
class Assignment (models.Model):
    assig_year = models.PositiveSmallIntegerField()
    customer = models.ForeignKey("Customer", on_delete=models.CASCADE)
    agent = models.ForeignKey("Agent", on_delete=models.CASCADE)
    
class Agent (models.Model):
    name = models.CharField(max_length=32)
    surname = models.CharField(max_length=32)
    
class Customer (models.Model):
    name = models.CharField(max_length=64)

Мне нужно предоставить пользователям настраиваемую форму администратора, которая позволяет создавать и обновлять заказы более интуитивно. Для этого я исключаю поле назначения (которое связывает клиента с торговым агентом за данный год, но может не быть таким очевидным для пользователя) и включаю настраиваемое поле для выбора только клиента. На основе этого ввода и даты заказа я внутренне ищу конкретное назначение, которое соответствует этим критериям, и присваиваю его заказу для сохранения в базе данных. Это выглядит так:

admin.py

from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.contrib import admin
from django.db.models import CharField
from django import forms
from django.db import models
from .models import Customer, Assignment, Order

class OrderForm(forms.ModelForm):
    
    customers_list = forms.ModelChoiceField(queryset = Customer.objects.all())
    
    class Meta:
        model = Order
        exclude = (
            'assignment',
        )
    
    def save(self, commit=True):
        # Создаем экземпляр заказа, но пока не сохраняем в БД
        obj: Order = super().save(commit=False)

        # Ищем связанное назначение. Если его нет, возникает ошибка
        try:
            assignment = Assignment.objects.get(assig_year=obj.order_date.year, customer=self.cleaned_data['customers_list'])
        except ObjectDoesNotExist:
            raise ValidationError('Не найдено назначение')

        # Добавляем ссылку на назначение в заказ и сохраняем его
        obj.assignment = assignment
        obj.save()

        return obj
        
    def clean(self):
        cleaned_data = self.cleaned_data
                
        date_form = cleaned_data['order_date']
        customer_form = cleaned_data['customers_list']
        
        try:
            cleaned_data['assignment'] = Assignment.objects.get(assig_year=date_form.year, customer=customer_form.id)
        except ObjectDoesNotExist:
            raise ValidationError('Не найдено назначение')
        
        # Затем вызываем метод clean() суперкласса
        super(OrderForm, self).clean()

        # Наконец, возвращаем очищенные данные
        return cleaned_data

class OrderAdmin(admin.ModelAdmin):
    form = OrderForm
    
    fieldsets = (
      ('Данные заказа:', {
          'fields': ( 'customers_list', 'qty', 'order_date', 'price', )
      }),
    ) 

Все работает нормально при создании нового заказа; однако, если я вхожу в ранее созданную запись в режиме администратора, настраиваемое поле клиента не заполняется автоматически, и я не знаю, как восстановить его исходное значение. Есть идеи?

Ответ или решение

Восстановление пользовательского поля при редактировании формы администратора Django

При работе с кастомными формами в админ-интерфейсе Django может возникнуть проблема с восстановлением значений пользовательских полей при редактировании существующих записей. В вашем случае поле выбора клиента (customers_list) не заполняется автоматически при редактировании ранее созданного объекта заказа. Давайте разберёмся, как это можно исправить.

Проблема

Когда вы создаете новый объект Order, поле customers_list успешно заполняется, поскольку оно может получить данные из ModelChoiceField. Однако, при редактировании, поле не восстанавливает значение связанного с ним объекта Customer, так как оно не указано в initial данных формы.

Решение

Для того чтобы исправить это, необходимо переопределить метод __init__ в вашей форме OrderForm. В этом методе мы можем установить начальное значение customers_list, основываясь на значении customer из связанного объекта Order. В результате, при редактировании, форма будет автоматически заполнять поле клиента.

Вот как вы можете изменить вашу форму:

class OrderForm(forms.ModelForm):
    customers_list = forms.ModelChoiceField(queryset=Customer.objects.all(), required=True)

    class Meta:
        model = Order
        exclude = ('assignment',)

    def __init__(self, *args, **kwargs):
        super(OrderForm, self).__init__(*args, **kwargs)
        if self.instance and self.instance.pk:
            # Если это существующий объект, устанавливаем начальное значение для customers_list
            self.initial['customers_list'] = self.instance.assignment.customer

    def save(self, commit=True):
        # Создаем экземпляр Order, но не сохраняем в БД, пока не проверим все данные
        obj: Order = super().save(commit=False)

        # Ищем связку Assignment и создаем ошибку, если её нет
        try:
            assignment = Assignment.objects.get(assig_year=obj.order_date.year, customer=self.cleaned_data['customers_list'])
        except ObjectDoesNotExist:
            raise ValidationError('Не удалось найти соответствующее назначение.')

        # Добавляем ссылку на задание в заказ и сохраняем
        obj.assignment = assignment
        if commit:
            obj.save()

        return obj

    def clean(self):
        cleaned_data = super().clean()
        date_form = cleaned_data.get('order_date')
        customer_form = cleaned_data.get('customers_list')

        try:
            cleaned_data['assignment'] = Assignment.objects.get(assig_year=date_form.year, customer=customer_form.id)
        except ObjectDoesNotExist:
            raise ValidationError('Не удалось найти соответствующее назначение.')

        return cleaned_data

Объяснение изменений

  1. Метод __init__:

    • В этом методе мы проверяем, является ли объект существующим (проверка self.instance and self.instance.pk). Если это так, мы устанавливаем начальное значение для поля customers_list, используя информацию о клиенте из связанного Assignment.
  2. Проверка аргумента commit в методе save:

    • Это гарантирует, что изменения будут применены только если флаг commit установлен в True. При редактировании объекта в админке, такая проверка может быть полезна для тестирования.
  3. Обновление метода clean:

    • Теперь он пытается получить Assignment, и если его нет, формируется соответствующее сообщение об ошибке.

Заключение

Обновляя метод инициализации формы, вы можете обеспечить автоматическую подстановку значения клиентского поля при редактировании существующего объекта Order. Это значительно упростит процесс для ваших пользователей, позволяя им быстрее и интуитивно находить нужные данные. Если у вас возникнут дополнительные вопросы или сложности, не стесняйтесь обращаться за помощью!

Оцените материал
Добавить комментарий

Капча загружается...

  1. Гость

    Отличная статья! Я столкнулся с похожей проблемой в одном из проектов и хотел бы предложить альтернативное решение.

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

    class OrderForm(forms.ModelForm):
    customers_list = forms.ModelChoiceField(queryset=Customer.objects.all(), required=True)

    class Meta:
    model = Order
    exclude = ('assignment',)

    def get_initial_for_field(self, field, field_name):
    if field_name == 'customers_list' and self.instance.pk:
    return self.instance.assignment.customer
    return super().get_initial_for_field(field, field_name)

    Этот подход автоматически заполняет поле customers_list при редактировании существующего объекта без необходимости дополнительной логики в методе __init__.

    Кроме того, обратите внимание на порядок вызова методов в вашем коде. В методе clean рекомендуется сначала вызвать метод суперкласса, а затем выполнять свою логику:

    def clean(self):
    cleaned_data = super().clean()
    # ваша проверка здесь
    return cleaned_data

    Это гарантирует, что базовые проверки формы выполнятся перед вашими кастомными, что может предотвратить потенциальные ошибки.

    Также, в методе save не забудьте учитывать параметр commit при сохранении объекта:

    if commit:
    obj.save()

    Это полезно, если форма используется в контексте, где непосредственное сохранение не требуется.

    Надеюсь, эти советы помогут улучшить ваш код! Спасибо за подробное описание проблемы и решения.

    Ответить