You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django-admin字段联动配置问题:instance为None引发AttributeError

Django Admin表单instance为None的报错解决

问题背景

我需要实现:SmartPhone模型的sd(BooleanField)为True时,sd_value(CharField)可编辑;当sd为False时,清空sd_value并禁用该字段。目前仅在商品创建页用JS实现了逻辑,但进入编辑页后字段状态会重置。

为了持久化状态,我编写了自定义Admin表单,但运行时报错:

AttributeError: 'NoneType' object has no attribute 'sd'

原因是kwargs.get('instance')返回了None,直接访问.sd导致错误。

完整代码

Admin.py

class SmartPhoneAdminForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if not kwargs.get('instance').sd:
            self.fields['sd_value'].widget.attrs.update({
                'readonly': True,
                'style': 'background: #050505;'
            })

    def clean(self):

        if not self.cleaned_data['sd']:
            self.cleaned_data['sd_value'] = None

        return self.cleaned_data

class SmartPhoneAdmin(admin.ModelAdmin):

    change_form_template = 'admin.html'
    form = SmartPhoneAdminForm
    prepopulated_fields = {'slug':('title',)}

    def formfield_for_foreignkey(self, db_field, request, **kwargs):

        if db_field.name == 'category':
            return ModelChoiceField(Category.objects.filter(slug = 'smartphone'))

        return super().formfield_for_foreignkey(db_field, request, **kwargs)
    
admin.site.register(SmartPhone, SmartPhoneAdmin)
admin.site.register(Category)

Models.py

def get_product_url(obj, viewname):
    
    ct_model = obj.__class__._meta.model_name
    return reverse(viewname, kwargs = {'ct_model': ct_model, 'slug': obj.slug})

class Category(models.Model):

    name = models.CharField(max_length = 255)
    slug = models.SlugField(unique = True)

    def __str__(self) -> str:
        return self.name
    
class Product(models.Model):
    
    # in pixels
    MIN_WIDTH = 400
    MIN_HEIGHT = 400
    MAX_WIDTH = 2500
    MAX_HEIGH = 2500

    # in bytes
    MIN_IMAGE_SIZE = 1024
    MAX_IMAGE_SIZE = 3145728

    class Meta:
        abstract = True

    category = models.ForeignKey(Category,
                                 verbose_name = 'Категория',
                                 on_delete = models.CASCADE)
    title = models.CharField(max_length = 255)
    slug = models.SlugField(unique = True)
    image = models.ImageField(upload_to = '',verbose_name = 'Изображение')
    description = models.TextField(verbose_name = 'Описание', null = True)
    price = models.DecimalField(max_digits = 9,
                                decimal_places = 2,
                                verbose_name = 'Цена')
    
    def save(self, *agrs, **kwargs) -> None:

        image = self.image
        img = Image.open(image)

        if img.height <  self.MIN_HEIGHT or img.width <  self.MIN_WIDTH:
            raise MinResolutionException ('Разрешение изображения меньше минимального!')
        
        elif img.height > self.MAX_HEIGH or img.width > self.MAX_WIDTH:
            new_img = img.convert('RGB')
            resize_new_img = new_img.resize((2500,2500), Image.BILINEAR)
            file_stream = BytesIO()
            resize_new_img.save(file_stream,'JPEG', quality = 90)
            file_stream.seek(0)
            name = '{}.{}'.format(*self.image.name.split('.'))
            self.image = InMemoryUploadedFile(file_stream,
                                              'ImageField',
                                              name,
                                              'jpeg/image',
                                              sys.getsizeof(file_stream),
                                              None)
        
        super().save(*agrs, **kwargs)
    
    def __str__(self) -> str:
        return self.title

class SmartPhone(Product):

    diagonal = models.CharField(max_length = 255, verbose_name = 'Диагональ')
    display_type = models.CharField(max_length = 255, verbose_name = 'Тип дисплея')
    resolution = models.CharField(max_length = 255, verbose_name = 'Разрешение экрана')
    aсcum_value = models.CharField(max_length = 255, verbose_name = 'Объем батареи')
    ram = models.CharField(max_length = 255, verbose_name = 'Оперативная память')
    sd = models.BooleanField(verbose_name = 'Наличие SD карты')
    sd_value = models.CharField(max_length = 255,
                                verbose_name = 'Максимальный объем встраиваемой памяти',
                                null = True,
                                blank = True)
    main_cam_mp = models.CharField(max_length = 255, verbose_name = 'Главная камера')
    frontal_cam_mp = models.CharField(max_length = 255, verbose_name = 'Фронтальная камера')

    def __str__(self) -> str:
        return '{} : {}'.format(self.category.name, self.title)
    
    def get_absolute_url(self):

        return get_product_url(self, 'product_detail')

问题原因

当创建新的SmartPhone对象时,表单不会传入instance参数(因为还没有对应的数据库记录),此时kwargs.get('instance')返回None,直接访问.sd就会触发AttributeError。只有编辑已有对象时,instance才会被传入并指向数据库中的记录。

修复方案

修改SmartPhoneAdminForm的__init__方法,先判断instance是否存在,再处理字段属性;同时完善clean方法的逻辑:

class SmartPhoneAdminForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 先获取instance,仅当instance存在且sd为False时禁用字段
        instance = kwargs.get('instance')
        if instance is not None and not instance.sd:
            self.fields['sd_value'].widget.attrs.update({
                'readonly': True,
                'style': 'background: #e9ecef;'  # 替换为更贴合Admin的禁用背景色
            })

    def clean(self):
        # 先调用父类clean方法,确保获取完整的cleaned_data
        cleaned_data = super().clean()
        # 检查sd字段状态,注意用get避免KeyError
        if not cleaned_data.get('sd'):
            cleaned_data['sd_value'] = None
        return cleaned_data

额外优化

为了让创建页也能实时响应sd的勾选状态,在自定义的admin.html中添加JS逻辑:

<script>
document.addEventListener('DOMContentLoaded', function() {
    const sdCheckbox = document.querySelector('input[name="sd"]');
    const sdValueInput = document.querySelector('input[name="sd_value"]');

    function toggleSdValueState() {
        sdValueInput.disabled = !sdCheckbox.checked;
        if (!sdCheckbox.checked) {
            sdValueInput.value = '';
        }
    }

    // 初始化状态
    toggleSdValueState();
    // 监听勾选变化
    sdCheckbox.addEventListener('change', toggleSdValueState);
});
</script>

内容的提问来源于stack exchange,提问作者Apple

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:56:58