Forms fields based on database content

Hey,

Basically, I need to create a form (an inline formset actually), that contains 1 input field (a %) with an associated label/description (to help user), as well as some foreign keys. How many times, and what the description should be for each input field depends on another table. Some models.py pseudo-code to give an idea:


class MetaGroup(Model)
	description = CharField(....)
	< other irrelevant fields >

class Discounts(Model)
	fk_metagroup = ForeignKey(MetaGroup, ...)
	fk_client = ForeignKey(Client, ....)
	percentage = Decimal(....)

The CONTENT of the MetaGroup table is what determines the things on which discounts could be applied. So basically, an discount entry is related to a specific client (fk_client) and to a specific MetaGroup (fk_metagroup). The description comes from fk_metagroup.description, and then the % determines what the discount is for that client with items from that MetaGroup. That all gets saved in the Discounts table, which is basically a list of all the discounts for on all the metagroups for all the clients. (I know this isn’t a very good structure to store that information, but that’s the way it’s been built and we’re not refactoring that now, so…)

For instance, the content of MetaGroup would look like:

image

In which case, my DiscountsForm would display 3 rows, with Plastic cable, Led Lights, Actuator in the 1st column, and the input fields would be 1 decimal fields for each (the applicable discount on each of those group). In a separate form, the user can create a new group (say, pk=4, Description=Halogene Lights). In that case, my Discounts forms would need to offer another row for the new MetaGroup “Halogene Lights”, etc. The Discount formset actually appears within the ClientForm as an inline_formset of the ClientForm, so setting the fk_client is straightforward. I will have to manually set the fk_metagroup manually when saving the form however.

I’m not sure what the best way to go about this is. Currently thinking something like:

class DiscountsForm(ModelForm):
    class Meta:
        model = Discount
        fields = ["percentage"]

DiscountFormset = inlineformset_factory(Client, Discount,
                                        form=DiscountForm,
                                        extra=1,
                                        can_delete=False)

However… a few issues:

  • extra=… isn’t a set number. I guess I could set it to some callback function (e.g. extra=how_many_discounts, with def how_many_discounts(): defined above in forms.py, checking with the MetaGroup how many forms needs to be in the formset.

  • I will need to put the associated descriptions (and the associated metagroup.pk so I can set fk_metagroup when saving each DiscountForm instance) in the context for the template to build the table to display to the user somehow. I’m thinking an inclusion_tag could do that. But then I’m somewhat spreading out the logic for how that form works. Can I add a method to my DiscountForm class and call that from the template to manage this?

Assume I get, say 4 discounts groups as per example above, along with 4 input fields from my DiscountsForm, then I would iterate on that in the template with a for loop, building my table. When the user saves the Client form, I will have to manage adding fk_metagroup to each new Discount instance (overriding form_valid() or save() or something like that the view… using a CreateView/Edit).

Any better ideas? Things I’m not seeing?

I’m sorry, I’m having a really difficult time trying to figure out exactly what you’re trying to determine here.

The closest I’ve come so far is that what you’re really trying to do is create a formset, where one of the fields in each form is pre-assigned a value.

Am I anywhere close to being right or have I missed the boat completely?

Well, yes, there is that - e.g. the fk_groupe. (And fk_client I guess, but that’s dealt with when I save the form, formset.instance = self.object). But that’s not really the crux of the matter for me.

The crux of the matter is that each form instance in the formset corresponds to 1 entry in MetaGroup table (e.g. Plastic cable, LED lights, etc.). Thus that means that the fk_groupe for each form in the formset must be set to the MetaGroup instance’s pk. So I guess that either in the template, or in an inclusion_tag, or the form view, or perhaps class DiscountForm, I need to:

  • Fetch the content of the MetaGroup table
  • create/add a form to the formset corresponding each entry returned (LED Lights, Plastic cable, etc.)
  • set the fk_group for that form

But then, the inlineformset_factory produces forms within a formset, but none of those forms will have any fk_groupe assigned. So somehow I would have to assign them at some point, but I’m not sure what hook I have to actually do that. So my current approach would be to make that information (a list of fk_groupe) available in the context to the template, and then assign it to each form instance as I iterate? (Or javascript would work & just .val() on each td).

I’m getting the pieces toghether to make that work. Thought it does seem complicated. I’m not really sure where I want to put the logic for that either.

What I think you might be looking for is Passing custom parameters to formset forms. In general, you’ll want to assign a different MetaGroup to each instance of the form, and mark that field as either read-only or disabled. (In this case, I would tend to suggest disabled.)

This topic was also discussed here: Pass different parameters to each form in formset . (The author of the first comment also makes reference to: https://datalowe.com/post/formsets-tutorial-1/.)

1 Like

Hey Ken

Amazing. Yes that would be one way to do it. I basically inherit from BaseFormset, modify the get_kwargs() method.

Then each form instance in my formset would have it’s data provided this way, so I could just do {{form.groupe.description}} in the template & just have that display properly in my table.

That does seem like the better to do this. I didn’t think it was possible to pass variying different arguments to each form in a formset, but that does seem to provide a roadmap to do it.

Just thought I’d add a few code snippets, for others who may be looking for something similar. Does take a bit of experimenting & being pointed to the right sections in the docs (slightly more obscure).

So essentially, you first need to pass whatever data you need to your Formset constructor - so likely in the get_context_data() of your create view. For instance:

class CreateCustomerView(LoginRequiredMixin, CreateView):
    template_name = "admin_account/create_customer.html"
    model = Client
    form_class = ClientForm
    login_url = '/login'
    context_object_name = "client"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        if self.request.POST:
            context["form"] = ClientForm(self.request.POST)
            context["formset_groupesc"] = GroupeEscFormset(self.request.POST)
            context["formset_groupesc2"] = GroupeEsc2Formset(self.request.POST)
        else:
            group_details = list(Groupe.objects.order_by("desc"))
            group2_details = list(Groupe2.objects.order_by("desc"))
            # we need - fk_group, fk_group.description,
            details = [[g.id, g.desc] for g in group_details]
            details2 = [[g.id, g.desc] for g in group2_details]

            formset = GroupeEscFormset(form_kwargs={'group_details':details})
            formset2 = GroupeEsc2Formset(form_kwargs={'group_details':details2})
            context["form"] = ClientForm()
            context["formset_groupesc"] = formset
            context["formset_groupesc2"] = formset2

        return context

The key bit above is that when I instantiate my formset, I pass it as argument whatever data I need them to use - in my case the ID the each inline form will need for the foreign key, and the description I want to show the users. So basically a list of tuples here.

Then, you’ll need to extend BaseInlineFormset:


class BaseGroupEscInlineFormset(BaseInlineFormSet):
    def get_form_kwargs(self, index):
        """ this BaseInlineFormset method returns kwargs provided to the form.
            in this case the kwargs are provided to the GroupEscForm constructor
        """
        kwargs = super().get_form_kwargs(index)
        try:
            group_details = kwargs['group_details'][index]
        except KeyError as ex:                                    # likely this is a POST, but the data is already in the form
            group_details = []
        return {'group_details':group_details}


GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
                                         form=GroupeEscForm,
                                         formset=BaseGroupEscInlineFormset,
                                         extra=len(Groupe.objects.all()),
                                         can_delete=False)

You can dig the documentation/django source code if you want. But what you really need to know is that when a form instance calls get_form_kwargs, by default it returns all the form kwargs. Which in my case would be all the tuples in my group_details list. But when it calls, it also gives the index of the calling form instance (e.g. whatever # from 0 to TOTAL_NUM_FORMS). Since I arranged my data to have 1 tuple for each form in the formset, I can simply use the index passed to return only the relevent data.

Then you can use your data however you need in your template:

<table id="{{table_id}}" class="table table-bordered dbase-table"  style="width:100%" cellspacing="0">
	<thead>
		<tr>
			<th>Description</th>
			<th>%</th>
			<th style="display:none;">Groupe</th>
			<th style="display:none;">Client</th>

		</tr>
	</thead>
	<tbody>
		{{ formset.management_form }}
		{{ formset.non_form_errors }}
		{% for form in formset %}
			<tr>
				<td>{{form.group_details.1}}</td>
				<td>{{form.pourcent}}</td>
				<td style="display:none;">
					<input  id="{{form.prefix}}-fk_groupe" type="hidden" name="{{form.prefix}}-fk_groupe" value="{{form.group_details.0}}">
				</td>
				<td style="display:none;">
					<input id="{{form.prefix}}-fk_client" type="hidden" name="{{form.prefix}}-fk_client">
				</td>
			</tr>
		{% endfor %}
	</tbody>
</table>
1 Like

Just an update - turns out that

GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
                                         form=GroupeEscForm,
                                         formset=BaseGroupEscInlineFormset,
                                         extra=len(Groupe.objects.all()),
                                         can_delete=False)

is not so gravy. More specifically, extra=len(Groupe.objects.all()) causes an issue if you pull your code brand new and try to makemigrations on a brand new DB.

In that case, you will get a "no such table: Groupe" error for makemigration. The issue seems to be that, within its basic checks before actually doing anything, Django will somehow pickup on that line, and check that the query actually runs. However, if this is a brand new DB, there’s no Groupe table yet (we’re trying to create it!). The check fails, and thus makemigrations as well.

Unsure what to do instead - maybe by adding another level of indirection it could pass under the radar. Or obviously just commenting the line, creating the DB and then uncommenting, but that’s not exactly best practice.

Hi logikonabstractions,

Did you consider moving that call into the view(s)? You’re absolutely correct, defining that at the module level will cause problems. The other benefit of moving it into the view is that as new groups are added, the extra parameter will match the true count of Groupe in your database. If you continue to define it at the module level, it will always be the number of Groupe that existed when your server process started.

Let me know if you have any questions.

-Tim

1 Like

Hey Tim,

Yeah that’s probably smarter. But I want to keep the formset definition in forms.py… so what, would you just add a method to the view that returns the # of object? Would that work? Or is there a way I can set the # of extra forms to put in the formset direclty in the view? I can’t think of it from the top of my mind, but surely that’s possible…

You’ve got a couple of options here:

  • Create a “factory method” which calls the formset factory, returning the value.
def GroupeEscFormset():
    return inlineformset_factory(...)

(Note, this creates the ugly looking line:
my_formset = GroupeEscFormset()(parameters for the formset call)
This can be avoided by accepting the parameters in the function definition and calling the formset creation in your factory function.)

In general, this should avoid that specific situation.

  • When you create the instance of the formset class, the forms aren’t created at that time - you can set the instance variables before creating the forms.

For example, you have the following line in your view:
formset = GroupeEscFormset(form_kwargs={'group_details':details})
At the point that this statement executes, the forms have not yet been created! Django uses a “lazy construction” for the forms, allowing you to alter the attributes of the formset.
If you add the following line after that line above, it should alter the number of extra forms:
formset.extra = 15
You’ll get 15 extra forms when they’re finally created.
(You do need to make sure you execute that line before you make any access to any individual form. The first reference to a form in the formset causes them to be rendered.)

1 Like

For example, you have the following line in your view:
formset = GroupeEscFormset(form_kwargs={'group_details':details})
At the point that this statement executes, the forms have not yet been created! Django uses a “lazy construction” for the forms, allowing you to alter the attributes of the formset.

That bit is particularly helpful, not only for the case at hand but more generally wrt form instantiation. In fact Django in general - I guess I don’t really tend to keep in mind that lots of objects are lazy in django (like fields in forms for instance), so often the best answer to deeper question might just be to consider if the evaluation will be lazy and thus if I can alter the object along the way… Will keep that in mind, thanks!