Search code examples
pythondjangodjango-modelsdjango-forms

Custom MultiInput Model Field in Django


I'm trying to create a custom field for volume made up of 4 parts (length, width, height and units). I'm thinking that extending the models.JSONField class makes the most sense.

Here is what I have so far.

inventory/models.py

from django.db import models
from tpt.fields import VolumeField

class Measurement(models.Model):
    volumne = VolumeField()

tpt/fields.py

from django.db import models
from tpt import forms
    
class VolumeField(models.JSONField):
    description = 'Package volume field in 3 dimensions'
    
    def __init__(self, length=None, width=None, height=None, units=None, *args, **kwargs):
        
        self.widget_args = {
            "length": length,
            "width": width,
            "height": height,
            "unit_choices": units,
        }

        super(VolumeField, self).__init__(*args, **kwargs)        
        
    def formfield(self, **kwargs):
        defaults = {"form_class": forms.VolumeWidgetField}
        defaults.update(kwargs)
        defaults.update(self.widget_args)
        return super(VolumeField, self).formfield(**defaults)

tpt/forms.py

import json
from django import forms

class VolumeWidget(forms.widgets.MultiWidget):
    def __init__(self, attrs=None):
        widgets = [forms.NumberInput(),
                   forms.NumberInput(),
                   forms.NumberInput(),
                   forms.TextInput()]
        super(VolumeWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return json.loads(value)
        else:
            return [0, 0, 0, '']

class VolumeWidgetField(forms.fields.MultiValueField):
    widget = VolumeWidget

    def __init__(self, *args, **kwargs):
        list_fields = [forms.fields.CharField(max_length=8),
                       forms.fields.CharField(max_length=8),
                       forms.fields.CharField(max_length=8),
                       forms.fields.CharField(max_length=4)]
        super(VolumeWidgetField, self).__init__(list_fields, *args, **kwargs)

    def compress(self, values):                                                
        return json.dumps(values)

I'm able to run the server but when I try and add a new entry to the Measurement Model in Admin I get this error:

[13/May/2024 11:57:59] "GET /admin/inventory/measurement/ HTTP/1.1" 200 12250
Internal Server Error: /admin/inventory/measurement/add/
Traceback (most recent call last):
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 716, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/utils/decorators.py", line 188, in _view_wrapper
    result = _process_exception(request, e)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/utils/decorators.py", line 186, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/views/decorators/cache.py", line 80, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/sites.py", line 240, in inner
    return view(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 1945, in add_view
    return self.changeform_view(request, None, form_url, extra_context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/utils/decorators.py", line 48, in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/utils/decorators.py", line 188, in _view_wrapper
    result = _process_exception(request, e)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/utils/decorators.py", line 186, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 1804, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 1838, in _changeform_view
    fieldsets = self.get_fieldsets(request, obj)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 404, in get_fieldsets
    return [(None, {"fields": self.get_fields(request, obj)})]
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 395, in get_fields
    form = self._get_form_for_get_fields(request, obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 786, in _get_form_for_get_fields
    return self.get_form(request, obj, fields=None)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 837, in get_form
    return modelform_factory(self.model, **defaults)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/forms/models.py", line 652, in modelform_factory
    return type(form)(class_name, (form,), form_class_attrs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/forms/models.py", line 310, in __new__
    fields = fields_for_model(
             ^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/forms/models.py", line 239, in fields_for_model
    formfield = formfield_callback(f, **kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/contrib/admin/options.py", line 228, in formfield_for_dbfield
    return db_field.formfield(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/Development/django/tpt/tpt/fields.py", line 22, in formfield
    return super(VolumeField, self).formfield(**defaults)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/db/models/fields/json.py", line 159, in formfield
    return super().formfield(
           ^^^^^^^^^^^^^^^^^^
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/db/models/fields/__init__.py", line 1145, in formfield
    return form_class(**defaults)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/stu/Development/django/tpt/tpt/forms.py", line 26, in __init__
    super(VolumeWidgetField, self).__init__(list_fields, *args, **kwargs)
  File "/Users/stu/.local/share/virtualenvs/storefront-QvD1zd1X/lib/python3.12/site-packages/django/forms/fields.py", line 1087, in __init__
    super().__init__(**kwargs)
TypeError: Field.__init__() got an unexpected keyword argument 'encoder'
[13/May/2024 11:58:01] "GET /admin/inventory/measurement/add/ HTTP/1.1" 500 179729

I've tried extending models.CharField to test but I'm then getting an error CharFields must define a 'max_length' attribute. and when I do specify a max_length I get a similar result to the JSONField but with max_length instead of encode: TypeError: Field.__init__() got an unexpected keyword argument 'max_length'

Any ideas what I'm doing wrong?

Thanks


Solution

  • After a little investigation I found that passing kwargs back to the VolumeWidgetField class was the cause of my issue. Simply removing this allowed the code to run as expected, but I think a better way to deal with this is to just remove the unwanted attributes from kwargs.

    Here is the updated tpt/forms.py

    import json
    from django import forms
    
    class VolumeWidget(forms.widgets.MultiWidget):
        template_name = 'stock/volume_template.html'
    
        def __init__(self, attrs=None):
            widgets = {'length': forms.NumberInput(),
                       'width': forms.NumberInput(),
                       'height': forms.NumberInput(),
                       'units': forms.Select(attrs=attrs, choices=[('mm','mm'), ('cm','cm'), ('m','m')])}
            super(VolumeWidget, self).__init__(widgets, attrs)
        
        def decompress(self, value):
            if value:
                data_list = json.loads(value)
                return data_list
            else:
                return [0, 0, 0, '']
            
        def get_context(self, name, value, attrs=None):
            if value:
                if not isinstance(value,list):
                    values = json.loads(value)
                else:
                    values = value
            else:
                values = [0, 0, 0, 'cm']
            return {'widget': {
                'name': name,
                'length': values[0],
                'width': values[1],
                'height': values[2],
                'units': values[3],
                'options': ['mm','cm','m']
            }}
    
    class VolumeWidgetField(forms.fields.MultiValueField):
        widget = VolumeWidget
    
        def __init__(self, *args, **kwargs):
            clean_kwargs = {k: v for k, v in kwargs.items()
                            if k != 'encoder' 
                            and k != 'decoder'
                            and k != 'length'
                            and k != 'width'
                            and k != 'height'
                            and k != 'units'
                            }
            list_fields = [forms.fields.CharField(max_length=32),
                           forms.fields.CharField(max_length=32),
                           forms.fields.CharField(max_length=32),
                           forms.fields.CharField(max_length=16)]
            super(VolumeWidgetField, self).__init__(list_fields, *args, **clean_kwargs)
    
        def compress(self, data_list):
            if not data_list:
                return None
            return json.dumps(data_list)
    

    Note the clean_kwargs variable.

    I hope this helps.