-1

In Django 1.9 I construct a list of forms based on a database table corresponding to TheModel in my forms.py. When the database table changes I want the forms to change as well. Therefore I set up a Django signal to listen to the change and update the forms. My problem is now that nothing can be really returned from the signal handler so I need to access the list of forms as a global variable. Since the use of global is generally considered bad practice: Is there a better solution for this or is the use of global in this case considered acceptable?

This is my code in views.py:

from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from .forms import construct_forms

# Init container for forms.
forms = construct_forms()
# Be aware that this signal is firiing twice for some reason.
@receiver((post_save, post_delete), sender=TheModel, dispatch_uid='change')
def reconstruct_forms(sender, **kwargs):
    """
    Reconstruct forms if model was changed.
    """
    # Use global variable so `forms` will be available in 
    # updated version later on.
    global forms
    forms = construct_forms()
    print('Model was changed, forms are reconstructed.')

def some_view(request):
    # Do something with the forms

Important edit:

At the time I asked this question I was completely unaware that signals only work in one thread of the running production server. Therefore an approach like this, using signals to update in memory variables will lead eventually lead to one thread on the server displaying the updated forms and the rest, where the signal did no reach, will show an outdated version. This is, of course, unacceptable in production. If caching is really required you should have a look at the Django docs on caching. For my little form construction it is actually overkill. I'll just leave this question to point in the right direction. Please, do not try to implement this my way!

Jarno
  • 6,243
  • 3
  • 42
  • 57
  • ekhm... changing tables (structure) or changing data? and why you need to rebuild forms after data change? – Jerzyk Jun 08 '16 at 12:27
  • 2
    and this will fail with bing bang with multiple threads or processes... aka production deployment :( – Jerzyk Jun 08 '16 at 12:30
  • Say I have a select widget with all the distinct options from a table column. If something is added I want this to reflected on the next load of the form, but since the data is rarely changed I want to cache the forms. – Jarno Jun 08 '16 at 12:31
  • Could you explain this or point to resource were it is explained? – Jarno Jun 08 '16 at 12:33
  • 1
    so you use distributed cache (memcache. redis) and you store there a *list* of choices, and then you update external cache in "save" or signal handler, you do not touch definition of the form, you just load data from cache in form's __init__ – Jerzyk Jun 08 '16 at 12:33
  • I'm afraid I'm not aware to the concepts of distributed and external cache. – Jarno Jun 08 '16 at 12:34
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/114128/discussion-between-jerzyk-and-jarno). – Jerzyk Jun 08 '16 at 12:35

1 Answers1

0

As @Jerzyk pointed out, caching the forms will only work in a single-threaded environment and will most likely fail in production. The signal is only sent and handled in the thread where the save/delete happened. Other processes will be unaware of the change.

Instead, store the query used to build the form in a shared cache, e.g. memcached or redis, using Django's cache framework.

from django.core.cache import cache

CHOICES_CACHE_KEY = 'choice_cache_key'

def get_cached_choices():
    choices = cache.get(CHOICES_CACHE_KEY)
    if choices is None:
        choices = ...  # Query the DB here
        cache.set(CHOICES_CACHE_KEY, choices, None)  # None caches forever
    return choices

def construct_forms(choices):
    forms = ...  # build forms with choices
    return forms

@receiver((post_save, post_delete), sender=TheModel, dispatch_uid='change')
def clear_choices_cache(sender, **kwargs):
    cache.delete(CHOICES_CACHE_KEY)

def some_view(request):
    # Do something with the forms
    forms = construct_forms(get_cached_choices())

However, I would only consider this solution if the query is expensive enough to justify the added complexity.

Daniel Hepper
  • 28,981
  • 10
  • 72
  • 75
  • 1
    I really do not understand why this is required, mostly because this code will fail when running in multiple threads/processes not mentioning even mutiple servers – Jerzyk Jun 08 '16 at 12:28
  • @Jerzyk you are right! I guess a correct solution would be to cache the query result in share cache. Will update my answer – Daniel Hepper Jun 08 '16 at 12:36
  • Since I found "We advise against storing the literal value None in the cache, because you won’t be able to distinguish between your stored None value and a cache miss signified by a return value of None." in https://docs.djangoproject.com/en/1.9/topics/cache/#the-low-level-cache-api I would maybe prefer something as `cache.set(CHOICES_CACHE_KEY, choices, 60 * 60 * 24 * 365)`. Otherwise great answer. – Jarno Jun 08 '16 at 13:55
  • 1
    That refers to passing None as value i.e. `cache.set(CHOICES_CACHE_KEY, None, 3600)`. Passing None as timeout is fine i.e. `cache.set(CHOICES_CACHE_KEY, choices, None)`. See two paragraphs above your citation in docs – Daniel Hepper Jun 10 '16 at 09:47