Sinidex
Sinidex

Reputation: 503

Initial Data for Django Inline Formsets

I have put together a form to save a recipe. It makes use of a form and an inline formset. I have users with text files containing recipes and they would like to cut and paste the data to make entry easier. I have worked out how to populate the form portion after processing the raw text input but I cannot figure out how to populate the inline formset.

It seems like the solution is almost spelled out here: http://code.djangoproject.com/ticket/12213 but I can't quite put the pieces together.

My models:

#models.py

from django.db import models

class Ingredient(models.Model):
    title = models.CharField(max_length=100, unique=True)

    class Meta:
        ordering = ['title']

    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        return self.id

class Recipe(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    directions = models.TextField()

    class Meta:
        ordering = ['title']

    def __unicode__(self):
        return self.id

    def get_absolute_url(self):
        return "/recipes/%s/" % self.id

class UnitOfMeasure(models.Model):
    title = models.CharField(max_length=10, unique=True)

    class Meta:
        ordering = ['title']

    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        return self.id

class RecipeIngredient(models.Model):
    quantity = models.DecimalField(max_digits=5, decimal_places=3)
    unit_of_measure = models.ForeignKey(UnitOfMeasure)
    ingredient = models.ForeignKey(Ingredient)
    recipe = models.ForeignKey(Recipe)

    def __unicode__(self):
        return self.id

The recipe form is created using a ModelForm:

class AddRecipeForm(ModelForm):
    class Meta:
        model = Recipe
        extra = 0

And the relevant code in the view (calls to parse out the form inputs are deleted):

def raw_text(request):
    if request.method == 'POST':

    ...    

        form_data = {'title': title,
                    'description': description,
                    'directions': directions,
                    }

        form = AddRecipeForm(form_data)

        #the count variable represents the number of RecipeIngredients
        FormSet = inlineformset_factory(Recipe, RecipeIngredient, 
                         extra=count, can_delete=False)
        formset = FormSet()

        return render_to_response('recipes/form_recipe.html', {
                'form': form,
                'formset': formset,
                })

    else:
        pass

    return render_to_response('recipes/form_raw_text.html', {})

With the FormSet() empty as above I can successfully launch the page. I have tried a few ways to feed the formset the quantity, unit_of_measure and ingredients I have identified including:

Any suggestions greatly appreciated.

Upvotes: 18

Views: 16568

Answers (4)

Abpostman1
Abpostman1

Reputation: 192

I had to handle an almost similar case for displaying stocks of each warehouse. The real available stock should be updated in the form taking in account transfers between stocks programmed but not validated yet by other employees.

So first, I am recording these transfers in a queryset that I pass to the form :

already_transferts = Transfert.objects.filter(
        product=product, processed=False).select_related('emetteur')

stock_to_fill = count_total_warehouses - count_current_product_warehouses

stock_inline_formset = inlineformset_factory(Produit, SstStock,
                                                 form=SstStockForm,
                                                 extra=stock_to_fill,
                                                 can_delete=False,
                                                 )
[...]
s_form = stock_inline_formset(
        instance=product, form_kwargs={'empty_sst': empty_sst,
       'user_is_staff': user.is_staff, 'already_transferts': 
       already_transferts})

Then, in the form __init__, I update the qtylike this:

class SstStockForm(forms.ModelForm):

    class Meta:
        model = SstStock
        fields = ('qty', 'warehouse', 'maxsst', 'adresse', 'pua', 'cau')

    def __init__(self, *args, **kwargs):
        self.empty_sst = kwargs.pop('empty_sst', None)
        self.user_is_staff = kwargs.pop('user_is_staff', None)
        # programmed transferts must be removed from availability
        self.already_transferts = kwargs.pop('already_transferts', None)
        super().__init__(*args, **kwargs)
        # if transfers are already programed for this product, remove their availability
        if self.already_transferts:
            for a in self.already_transferts:
                init = self.initial
                if init['warehouse'] == a.emetteur_id:
                    init['qty'] = init['qty'] - a.qty

Not sure this is the best/cleaner way but this works well.

Upvotes: 0

bahoo
bahoo

Reputation: 402

I'm having no trouble passing initial to the inline formset — not the factory method, crucially — circa Django 4.1, FWIW.

MyInlineFormSet = inlineformset_factory(ParentModel, ChildModel, ...)
formset = MyInlineFormSet(initial=[{...}])

It's been 12+ years since the original question, so eminently possible the codebase has changed, and/or maybe I'm misreading some of the answers and comments here, but hoping this clarification might be helpful to someone on the hunt for how to do it.

Upvotes: 2

Ezequiel Alanis
Ezequiel Alanis

Reputation: 451

I couldn't make Aram Dulyan code works on this

for subform, data in zip(formset.forms, recipe_ingredients):
    subform.initial = data

Apparently something changed on django 1.8 that i can't iterate a cached_property

forms - django.utils.functional.cached_property object at 0x7efda9ef9080

I got this error

zip argument #1 must support iteration

But i still take the dictionary and assign it directly to my formset and it worked, i took the example from here:

https://docs.djangoproject.com/en/dev/topics/forms/formsets/#understanding-the-managementform

from django.forms import formset_factory from myapp.forms import ArticleForm

ArticleFormSet = formset_factory(ArticleForm, can_order=True)
formset = ArticleFormSet(initial=[
    {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
    {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
])

My code on assign formset to template

return self.render_to_response(
self.get_context_data(form=form, inputvalue_numeric_formset=my_formset(initial=formset_dict)

Upvotes: 2

Aram Dulyan
Aram Dulyan

Reputation: 2434

My first suggestion would be to take the simple way out: save the Recipe and RecipeIngredients, then use the resulting Recipe as your instance when making the FormSet. You may want to add a "reviewed" boolean field to your recipes to indicate whether the formsets were then approved by the user.

However, if you don't want to go down that road for whatever reason, you should be able to populate your formsets like this:

We'll assume that you have parsed the text data into recipe ingredients, and have a list of dictionaries like this one:

recipe_ingredients = [
    {
        'ingredient': 2,
        'quantity': 7,
        'unit': 1
    },
    {
        'ingredient': 3,
        'quantity': 5,
        'unit': 2
    },
]

The numbers in the "ingredient" and "unit" fields are the primary key values for the respective ingredients and units of measure objects. I assume you have already formulated some way of matching the text to ingredients in your database, or creating new ones.

You can then do:

RecipeFormset = inlineformset_factory(
    Recipe,
    RecipeIngredient,
    extra=len(recipe_ingredients),
    can_delete=False)
formset = RecipeFormset()

for subform, data in zip(formset.forms, recipe_ingredients):
    subform.initial = data

return render_to_response('recipes/form_recipe.html', {
     'form': form,
     'formset': formset,
     })

This sets the initial property of each form in the formset to a dictionary from your recipe_ingredients list. It seems to work for me in terms of displaying the formset, but I haven't tried saving yet.

Upvotes: 26

Related Questions