Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pseudo-form in Django admin that generates a json object on save

I have a model with a field for a json object. This object is used on the site to control some css variables, among other things.

Right now in the admin, I have a text field where a user can save a json object. I'd like to show a form with all the attributes that, upon saving, will generate a json object.

Basically, the user sees, and the data is stored, like this:

{
    "name":"hookedonwinter",
    "user-id":123,
    "basics":{
        "height":150,
        "weight":150
        }
}

And I'd rather have the user see this:

Name: <input field>
User Id: <input field>
Height: <input field>
Weight: <input field>

and the data still be stored in json.

Any guidance would be appreciated. Links to docs that explain this, doubly appreciated.

Thanks!

like image 747
hookedonwinter Avatar asked Mar 02 '12 23:03

hookedonwinter


2 Answers

Idea

Basically what you need to do is render your JSON into fields.

  1. Create field for your model that stores JSON data.
  2. Create form field
  3. Create widget that:
    1. Renders fields as multiple inputs
    2. Takes data from POST/GET and transforms it back into JSON

You can also skip steps 1, 2 by overriding widget for TextField.

Documentation links

  • Widgets: https://docs.djangoproject.com/en/1.3/ref/forms/widgets/
  • Django code for reference how to create widgets: https://code.djangoproject.com/browser/django/trunk/django/forms/widgets.py

Proof of concept

I tried coding this solution and here is solution that worked for me without some edge cases.

fields.py

import json

from django.db import models
from django import forms
from django import utils
from django.utils.translation import ugettext_lazy as _


class JSONEditableField(models.Field):
    description = _("JSON")

    def formfield(self, **kwargs):
        defaults = {'form_class': JSONEditableFormField}
        defaults.update(kwargs)
        return super(JSONEditableField, self).formfield(**defaults)

class JSONEditableWidget(forms.Widget):
    def as_field(self, name, key, value):
        """ Render key, value as field """
        attrs = self.build_attrs(name="%s__%s" % (name, key))
        attrs['value'] = utils.encoding.force_unicode(value)
        return u'%s: <input%s />' % (key, forms.util.flatatt(attrs))

    def to_fields(self, name, json_obj):
        """Get list of rendered fields for json object"""
        inputs = []
        for key, value in json_obj.items():
            if type(value) in (str, unicode, int):
                inputs.append(self.as_field(name, key, value))
            elif type(value) in (dict,):
                inputs.extend(self.to_fields("%s__%s" % (name, key), value))

        return inputs

    def value_from_datadict(self, data, files, name):
        """
        Take values from POST or GET and convert back to JSON..
        Basically what this does is it takes all data variables
        that starts with fieldname__ and converts
        fieldname__key__key = value into json[key][key] = value
        TODO: cleaner syntax?
        TODO: integer values don't need to be stored as string
        """
        json_obj = {}

        separator = "__"

        for key, value in data.items():
            if key.startswith(name+separator):
                dict_key = key[len(name+separator):].split(separator)

                prev_dict = json_obj
                for k in dict_key[:-1]:
                    if prev_dict.has_key(k):
                        prev_dict = prev_dict[k]
                    else:
                        prev_dict[k] = {}
                        prev_dict = prev_dict[k]

                prev_dict[dict_key[-1:][0]] = value

        return json.dumps(prev_dict)


    def render(self, name, value, attrs=None):
        # TODO: handle empty value (render text field?)

        if value is None or value == '':
            value = '{}'

        json_obj = json.loads(value)
        inputs = self.to_fields(name, json_obj)

        # render json as well
        inputs.append(value)

        return utils.safestring.mark_safe(u"<br />".join(inputs))

class JSONEditableFormField(forms.Field):
    widget = JSONEditableWidget

models.py

from django.db import models
from .fields import JSONEditableField

class Foo(models.Model):
    text = models.TextField()
    json = JSONEditableField()

Hope this helps and here is how it looks: Result

like image 155
Marius Grigaitis Avatar answered Sep 30 '22 17:09

Marius Grigaitis


I had similar task. I resolved it by creating Django form widget. You can try it for yours applications django-SplitJSONWidget-form

like image 36
Abbasov Alexander Avatar answered Sep 30 '22 16:09

Abbasov Alexander