Opened 6 years ago

Last modified 4 months ago

#11390 new Bug

If you use a callable as default value on a model field, it gets called 3 times.

Reported by: espen.nettbruk@… Owned by:
Component: Forms Version: 1.0
Severity: Normal Keywords: models
Cc: danny.adair@… Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I belive i found a bug in the Django code, im truly sorry if it isnt. I just added a callable (a function) as the default value to a models field. And when i then initate the model, e.x by refreshing the admin edit page, I can se that the function runs three (3) times. There is no reason this should happen and it could lead to performance issues if the calulations done in this function is though.

Change History (16)

comment:1 Changed 6 years ago by russellm

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset
  • Triage Stage changed from Unreviewed to Accepted

I've verified that this is actually happening. Without digging into details, I'm pretty sure the three calls are caused by:

  1. Instantiating an empty Model() instance to pass to the form
  2. Rendering the visible form field, and therefore the value displayed
  3. Rendering the hidden form field providing a comparison initial-value.

It might be possible to collapse these calls into a single usage, but it will take some spelunking to sort out the details. In the meantime, the workaround is to keep your default callables lightweight (or, if they can't be lightweight, cache the computed values).

comment:2 Changed 4 years ago by julien

  • Severity set to Normal
  • Type set to Bug

comment:3 Changed 3 years ago by aaugustin

  • UI/UX unset

Change UI/UX from NULL to False.

comment:4 Changed 3 years ago by aaugustin

  • Easy pickings unset

Change Easy pickings from NULL to False.

comment:5 Changed 3 years ago by danny.adair@…

  • Cc danny.adair@… added

I would like to add that this not just a performance issue.
I believe it prevents any kind of reliable "counter" fields, includig an IntegerField primary key where some control is needed (i.e. not just autoincrement). See http://stackoverflow.com/a/9354703/640759 (postgresql-only solution)
Another example would be per-foreignkey counters, such as invoice numbers which increase per customer account. A workaround there would be to postpone the pulling of a new counter number until save(). See http://djangosnippets.org/snippets/1574/

comment:6 Changed 2 years ago by aaugustin

  • Component changed from Core (Other) to Forms

comment:7 follow-up: Changed 4 months ago by jdunck

Possibly fixed in #24391

comment:8 Changed 4 months ago by lorenzo-pasa

  • Owner changed from nobody to lorenzo-pasa
  • Status changed from new to assigned

comment:9 in reply to: ↑ 7 Changed 4 months ago by lorenzo-pasa

Replying to jdunck:

Possibly fixed in #24391

This bug is still there.
I just checked using the latest Django version, "pip installed" from the master branch (i.e. Django==1.9.dev20150322063655).

I created a model with a single field, and just added a function as the default value to this field.
Every time I access its add page in the admin, the method is called 3 times.

comment:10 Changed 4 months ago by lorenzo-pasa

  • Owner lorenzo-pasa deleted
  • Status changed from assigned to new

comment:11 Changed 4 months ago by tomviner

  • Owner set to tomviner
  • Status changed from new to assigned

comment:12 Changed 4 months ago by tomviner

ok, I've located the 3 calls to get the default value, that are made when you view the admin "add item" page. 2 are from the view and the 3rd from BoundField, when the form is rendered.

Regarding 2 calls in the view:

django/contrib/admin/options.py: ModelAdmin.changeform_view

line 1389:    form = ModelForm(initial=initial)
line 1390:    formsets, inline_instances = self._create_formsets(request, self.model(), change=False)

actually both these lines end up calling model(). ModelForm does it when setting ModelForm().instance and as you can see the 2nd line there has an explicit self.model().

So my thought is to simply replace the call to self.model() with form.instance. i.e.:

line 1389:    form = ModelForm(initial=initial)
line 1390:    formsets, inline_instances = self._create_formsets(request, form.instance, change=False)

See one line PR with this change. But it's hard to know if this has unintended effects? So feedback appreciated on that please.

As for the 3rd call in BoundField. I have no idea how to merge this with the one above, or if that would be desired anyway. The fact is, an in memory model object gets created with the default value, and a ModelForm field/widget also gets created with a fresh call to get the default.

My sample Django project is here, and includes stack traces from the 3 calls.

comment:13 Changed 4 months ago by tomviner

  • Has patch set

comment:14 Changed 4 months ago by Tim Graham <timograham@…>

In a4a58811:

Removed redundant model instantiation in contrib.admin; refs #11390.

comment:15 Changed 4 months ago by timgraham

  • Has patch unset

comment:16 Changed 4 months ago by tomviner

  • Owner tomviner deleted
  • Status changed from assigned to new

Thanks for the merge.

One down one to go. But as I mentioned earlier, I'm not sure how or if it's possible to combine the two remaining calls.

This simple example shows the 2 calls:

In [1]: from django.forms.models import modelform_factory

In [2]: from cad.models import Article

In [3]: article_modelform = modelform_factory(Article, fields=('title',))

In [4]: form = article_modelform()
get_default_title call #1

In [5]: form.as_p()
get_default_title call #2
Out[5]: u'<p>...

One's the in memory model being instantiated and the other's the ModelForm field/widget populating it's values. They seem separate and I'm not sure where any memoisation of this value would go or if it would be a great idea.

I'll unassign myself so others can either take a stab, or chime in that it's not a worthy goal.

Note: See TracTickets for help on using tickets.
Back to Top