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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 15:10:00