Search code examples
pythondjangodjango-modelsimportflat-file

Importing Denormalized data into django models via modelforms


The Scenario:

I have some data that looks a bit like this:

Person   | Favorite Color | Favorite Fruit
------------------------------------------
Bobby    | RED            | BANANA
Jared    | YELLOW         | RASPBERRY
Milly    | BLACK          | PEACH
Shawn    | ORANGE         | ORANGE

Assume it's in a flatfile, or python dicts, or some other non-sql format.

EDIT: Assume for the sake of argument that I've already got it in a Python structure that looks like this:

data = [
    {"name": "Bobby", "favorite_color": "RED", "favorite_fruit": "BANANA"},
    {"name": "Jared", "favorite_color": "YELLOW", "favorite_fruit": "RASPBERRY"},
    # etc....
 ]

I have django models that look like this:

class Person(models.Model):
    COLORS = (
                 ('R', 'RED'),
                 ('O', 'ORANGE'),
                 ('Y', 'YELLOW'),
                 ('G', 'GREEN'),
                 ('B', 'BLUE'),
                 ('P', 'PURPLE'),
                 ('L', 'BLACK'),
                 ('W', 'WHITE')
              )
    name = CharField(max_length=256)
    favorite_color = CharField(max_length=1, choices=COLORS)
    favorite_fruit = ForeignKey(Fruit)

class Fruit(models.Model):
    name = CharField(max_length=256)
    fructose_content = PositiveIntegerField()

EDIT: Assume that my Fruit model is already populated with all the possible fruits.

The task:

I would like to import my data from the original source into my Django models by using ModelForms, to take advantage of proper validation and database abstraction.

class PersonForm(forms.ModelForm):
    class Meta:
        model = Person
        fields = '__all__'

Is there a way the ModelForm can translate the denormalized data into data that can be saved in the model? Are ModelForms the wrong thing to use here?


Solution

  • I came up with a partial solution, at least for the problem involving the choices. I guess with some tinkering it could work for ForeignKey fields as well.

    First, I define a function get_choice_by_name which goes through a choices tuple and looks for a key by value.

    Then I subclassed TypedChoiceField and overrode its clean() method to transform the data. This method seems to get called before any validation.

    Here's the code:

    def get_choice_by_name(name, choices, case_sensitive=False):
        try:
            if name is None:
                return ''
            elif name and not case_sensitive:
                return next(k for k, n in choices
                            if n.lower() == name.lower())
            else:
                return next(k for k, n in choices if n == name)
        except StopIteration:
            raise ValueError(
                "Invalid choice: {}, not found in {}".format(name, choices)
            )
    
    class DenormalizedChoiceField(TypedChoiceField):
    
        def clean(self, value):
            if not value:
                return self.empty_value
            try:
                value = get_choice_by_name(value, self.choices)
            except ValueError as e:
                raise ValidationError(str(e))
    
            value = super(DenormalizedChoiceField, self).clean(value)
            return value
    

    My ModelForm now just needs to redefine the fields in question as DenormalizedChoiceField. I need to specify the choices explicitly, though, for some reason it doesn't pick this up from the model if you override the field.

    class PersonForm(forms.ModelForm):
        favorite_color = DenormalizedChoiceField(choices=Person.COLORS)
        class Meta:
            model = Person
            fields = '__all__'