Django BinaryField max_length验证触发翻译模块KeyError求助
Django BinaryField 上传超限文件触发翻译模块KeyError问题排查与解决
问题背景
在Django中定义了带max_length=500000参数的BinaryField模型字段,同时自定义表单字段BinaryFieldWithUpload实现Admin界面的图片上传功能。正常大小的图片上传无问题,但上传超过限制的文件时,虽触发验证却抛出翻译模块__init__.py中的KeyError错误,添加自定义MaxFileSizeValidator验证器后问题仍未解决。
模型初始代码
class Notenbild(models.Model): bild_data = models.BinaryField(max_length=500000, editable=True)
自定义表单字段与Admin配置
class BinaryFieldWithUpload(forms.FileField): def __init__(self, *, max_length=None, allow_empty_file=False, **kwargs): super().__init__(max_length=max_length, allow_empty_file=allow_empty_file, **kwargs) def to_python(self, data): data = super().to_python(data) if data: image = Image.open(data) # 省略其他图片处理逻辑 byte_array = io.BytesIO() image.save(byte_array, format='PNG') return byte_array.getvalue() return None def widget_attrs(self, widget): attrs = super().widget_attrs(widget) if isinstance(widget, FileInput) and "accept" not in widget.attrs: attrs.setdefault("accept", "image/*") return attrs @admin.register(Notenbild) class NotenbildAdmin(admin.ModelAdmin): fields = [ 'bild_data', 'vorschau', ] readonly_fields = ['vorschau'] formfield_overrides = { models.BinaryField: {'form_class': BinaryFieldWithUpload}, } @admin.display(description='Bild (Vorschau)') def vorschau(self, notenbild: Notenbild): encoded_image = base64.b64encode(notenbild.bild_data).decode('utf-8') return format_html( f'<p>{len(notenbild.bild_data)} bytes</p>' f'<img src="data:image/png;base64,{encoded_image}" style="max-width:40rem; max-height:16rem" />' )
报错信息
KeyError at /admin/library/notenbild/368/change/ "Your dictionary lacks key 'max'. Please provide it, because it is required to determine whether string is singular or plural." Django Version: 5.1.1 Exception Location: /Users/alex/Repositories/ekd-cms/venv/lib/python3.12/site-packages/django/utils/translation/__init__.py, line 130, in _get_number_value
完整栈追踪
Traceback (most recent call last): File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/utils/translation/__init__.py", line 128, in _get_number_value return values[number] ^^^^^^^^^^^^^^ During handling of the above exception ('max'), another exception occurred: File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) ^^^^^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/debug_toolbar/middleware.py", line 92, in __call__ panel.generate_stats(request, response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/debug_toolbar/panels/templates/panel.py", line 201, in generate_stats template_data["context_list"] = self.process_context_list( File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/debug_toolbar/panels/templates/panel.py", line 134, in process_context_list if key_values == context_layer: ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/forms/utils.py", line 192, in __eq__ return list(self) == other ^^^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/forms/utils.py", line 192, in __eq__ return list(self) == other ^^^^^^^^^^ File "<frozen _collections_abc>", line 1026, in __iter__ <source code not available> ^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/forms/utils.py", line 197, in __getitem__ return next(iter(error)) ^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/core/exceptions.py", line 210, in __iter__ message %= error.params ^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/utils/functional.py", line 167, in __mod__ return self.__cast() % other ^^^^^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/utils/translation/__init__.py", line 148, in __mod__ number_value = self._get_number_value(rhs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/alex/Repositories/my-project/venv/lib/python3.12/site-packages/django/utils/translation/__init__.py", line 130, in _get_number_value raise KeyError( ^
自定义验证器代码
from django.core.validators import BaseValidator from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError @deconstructible class MaxFileSizeValidator(BaseValidator): message = _("Ensure the file is less than or equal to %(limit_value)s.") code = "limit_value" def __init__(self, limit_value, message=None): super().__init__(limit_value, message) self.limit_value = limit_value if message: self.message = message def __call__(self, value): cleaned = self.clean(value) limit_value = ( self.limit_value() if callable(self.limit_value) else self.limit_value ) params = {"limit_value": limit_value, "show_value": cleaned, "value": value} if self.compare(cleaned, limit_value): raise ValidationError(self.message, code=self.code, params=params) def compare(self, a, b): return len(a) > b
修改后的模型代码
class Notenbild(models.Model): bild_data = models.BinaryField(max_length=500000, editable=True, validators=[MaxFileSizeValidator(500000)])
问题根源分析
这个KeyError是Django翻译系统在处理验证错误消息时触发的:
- 当
BinaryField的max_length验证失败时,Django默认的错误消息使用了复数化格式,要求参数中包含max键,但自定义表单字段BinaryFieldWithUpload继承自FileField,在传递验证参数时没有提供该键。 - 即使添加了自定义验证器,默认的
max_length验证逻辑仍然会执行,导致冲突。
解决思路与修复步骤
1. 覆盖表单字段的默认验证错误消息
在自定义BinaryFieldWithUpload中,重新定义max_length的错误消息,避免使用需要max参数的复数化模板:
class BinaryFieldWithUpload(forms.FileField): def __init__(self, *, max_length=None, allow_empty_file=False, **kwargs): super().__init__(max_length=max_length, allow_empty_file=allow_empty_file, **kwargs) # 覆盖max_length验证的错误消息 for validator in self.validators: if hasattr(validator, 'code') and validator.code == 'max_length': validator.message = _('文件大小不能超过 %(limit_value)s 字节。') validator.params = {'limit_value': max_length}
2. 禁用模型字段的默认max_length验证
既然已经添加了自定义验证器,或者通过表单字段处理了大小验证,可以在模型字段中禁用默认的max_length验证,避免重复验证冲突:
class Notenbild(models.Model): bild_data = models.BinaryField( max_length=500000, editable=True, validators=[MaxFileSizeValidator(500000)], # 禁用默认max_length验证 error_messages={'max_length': ''} )
3. 确保自定义验证器的消息参数正确
调整自定义验证器的消息模板,或者确保参数传递符合Django翻译系统的要求:
@deconstructible class MaxFileSizeValidator(BaseValidator): # 修改消息模板,避免复数化需求 message = _("文件大小不能超过 %(limit_value)s 字节。") code = "max_file_size" # 其余代码保持不变
4. 优先在表单层处理验证
由于自定义表单字段已经将文件转换为字节数组,更适合在表单层进行大小验证,而不是模型层:
class BinaryFieldWithUpload(forms.FileField): def __init__(self, *, max_length=None, allow_empty_file=False, **kwargs): super().__init__(max_length=max_length, allow_empty_file=allow_empty_file, **kwargs) # 添加自定义大小验证器到表单字段 self.validators.append(MaxFileSizeValidator(max_length)) # 移除默认的max_length验证器 self.validators = [v for v in self.validators if not (hasattr(v, 'code') and v.code == 'max_length')] # 其余代码保持不变
内容的提问来源于stack exchange,提问作者Alexander Pacha
相关产品推荐
相关产品推荐

