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
相关产品推荐
相关产品推荐

