[Fixed]-Django Proxy Field

2👍

In order to make the field virtual, you need to:

  1. Override the Field.get_attname_column() method, which must return two-tuple attname, None as the value for attname and column.
  2. Set the private_only parameter to True in the Field.contribute_to_class() method.

A proxy field must also have a reference to the concrete field in order to be able to access to it. Here I will use the concrete_field parameter.

class ProxyMixin(object):
    """
    A mixin class that must be mixed-in with model fields.
    
    The descriptor interface is also implemented in this mixin
    class to keep value getting/setting logic on the Model.
    """

    def __init__(self, *args, concrete_field=None, **kwargs):
        self._concrete_field = concrete_field
        super().__init__(*args, **kwargs)

    def check(self, **kwargs):
        return [
            *super().check(**kwargs),
            *self._check_concrete_field(),
        ]

    def _check_concrete_field(self):
        try:
            self.model._meta.get_field(self._concrete_field)
        except FieldDoesNotExist:
            return [
                checks.Error(
                    "The %s concrete field references the "
                    "nonexistent field '%s'." % (self.__class__.__name__, self._concrete_field),
                    obj=self,
                    id='myapp.E001',
                )
            ]
        else:
            return []

    def get_attname_column(self):
        attname, column = super().get_attname_column()
        return attname, None

    def contribute_to_class(self, cls, name, private_only=False):
        super().contribute_to_class(cls, name, private_only=True)
        setattr(cls, name, self)

    @property
    def concrete_field(self):
        """
        Returns the concrete Field instance.
        """
        return self.model._meta.get_field(self._concrete_field)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if self._concrete_field is not None:
            kwargs['concrete_field'] = self._concrete_field
        return name, path, args, kwargs

    def __get__(self, instance, owner=None):
        if instance is None:
            return self

        return getattr(instance, self._concrete_field)

    def __set__(self, instance, value):
        setattr(instance, self._concrete_field, value)

If you are sure that the concrete field represents a dict-like object, then you can change the logic for getting / setting value. Maybe something like this:

def __get__(self, instance, owner=None):
    if instance is None:
        return self

    data = getattr(instance, self._concrete_field) or {}
    return data.get(self.name, self.get_default())

def __set__(self, instance, value):
    data = getattr(instance, self._concrete_field)
    if data is None:
        setattr(instance, self._concrete_field, {})
        data = getattr(instance, self._concrete_field)
    data[self.name] = value
👤Sultan

1👍

There are proxy models in django, But I am not sure if it has something like proxy fields.

For your use case, you can do as mentioned below:

  1. Create a list of fields with each field containing name, type, nullable, etc.
  2. Add a function in your model to return actual django rest framework (DRF) field class instance, corresponding to each field type passed to it.
  3. Use DRF inbuilt field class validation to validate your field data against specified field type in save().
  4. In addition to automatic validation, you will also get automatic type conversion. Eg. If user entered number 1 as text: “1” for a integer field then it will automatically convert it back to integer 1. Same way, it will work for float, Bool, Char, etc

`

from django.db import models
from rest_framework.fields import IntegerField, FloatField, BooleanField, DateTimeField, CharField

class MyModel(models.Model):
    FIELDS = [{'field_name': 'proxy_int', 'field_type': 'int', 'null_allowed': 'True'},
              {'field_name': 'proxy_char', 'field_type': 'string', 'null_allowed': 'True'}]

    data = JsonField()

    def field_type(self, field):

        if field.field_type == 'int':
            return IntegerField()
        elif field.field_type == 'float':
            return FloatField()
        elif field.field_type == 'bool':
            return BooleanField()
        elif field.field_type == 'date':
            return DateTimeField()
        elif self.value_type == 'string':
            return CharField()
        return CharField()

    def save(self, *args, **kwargs):
        data = kwargs.get('data', {})
        new_data = {}

        for (field in FIELDS)

            field_name = field['field_name']
            field_type = field['field_type']
            field_value = data.get(field_name, None)

            validated_value = self.field_type(field_type).run_validation(field_value)

            new_data[field_name] = validated_value

        kwargs['data'] = new_data
        return super(MyModel, self).save(*args, **kwargs)`

You may try and figure out django’s field classes (Instead of DRF) and use them for validation, if required.

You can inherit this new MyModel class to achieve similar capability in other models and to reuse code.

Leave a comment