Search code examples
djangodjango-formsdjango-admin

How to add an editable field in the django admin that is not directly linked to a model field?


How to add a custom editable field in the django admin and then transform it before storing it in a field in the model?

Given this Model:

import json
from django.db import models
from django.core.serializers.json import DjangoJSONEncoder

class Foo(models.Model):
    _encrypted_data = models.TextField(null=True, blank=True)

    @property
    def data(self):
        encrypted = self._encrypted_data
        if not encrypted:
            return {}

        return json.loads(decrypt(encrypted))

    @data.setter
    def data(self, value):
        if not value:
            self._encrypted_data = {}
            return

        self._encrypted_data = encrypt(
            json.dumps(value, cls=DjangoJSONEncoder)
        )

The field Model._encrypted_data contains encrypted data.

The property Model.data makes it easy to work with the decrypted data in the code: encrypt/decrypt automatically.

I am looking for an editable admin field that represents the Model.data property.

I started to explore a custom Admin Form but I am not sure how to pass the decrypted data to the field and then save it on the model.

class FooAdminForm(forms.ModelForm):
    class Meta:
        model = Foo
        fields = '__all__'

    data = forms.JSONField(label='Data', required=False)

class FooAdmin(admin.ModelAdmin):
    form = FooAdminForm


Solution

  • You can define a custom model field, for example by subclassing a TextField:

    # app_name/fields.py
    
    
    class EncryptedJsonField(models.TextField):
        def to_python(self, value):
            value = super().to_python(value)
            if not value:
                return {}
            return json.loads(decrypt(value))
    
        def from_db_value(self, value, expression, connection):
            value = super().from_db_value(value, expression, connection)
            if not value:
                return {}
            return json.loads(decrypt(value))
    
        def get_db_prep_value(self, value, connection, prepared=False):
            if value:
                value = encrypt(json.dumps(value, cls=DjangoJSONEncoder))
            return super().get_db_prep_value(value, connection, prepared)

    and in the model use this as:

    class Foo(models.Model):
        data = EncryptedJsoNField()