Callable choices for django rest framework
At work I'm writing an API using Django/DRF. Suddenly I had to write an
application (just a few pages for calling a few endpoints), so I (ab)used DRF's
Serializer
s to build them. One of the problems I faced while doing this was
that DRF's ChoiceField
accepts only a sequence with the values for the
dropdown, unlike Django's, who also accepts callables. This means that once you
gave it a set of values, it never ever changes, at least until you restart
the application.
Unless, of course, you cheat. Or hack. Aren't those synonyms?
class UpdatedSequence: def __init__(self, update_func): self.update_func = update_func self.restart = True self.data = None self.index = 0 def __iter__(self): # we're our own iterator return self def __next__(self): # if we're iterating from the beginning, call the function # and cache the result if self.restart: self.data = self.update_func() self.index = 0 try: datum = self.data[self.index] except IndexError: # we reached the limit, start all over self.restart = True raise StopIteration else: self.index += 1 self.restart = False return datum
This simple class tracks when you start iterating over it and calls the function
you pass to obtain the data. Then it iterates over the result. When you reach
the end, it marks it to start all over, so the next time you iterate over it, it
will call the function again. The function you pass can be the all()
method of
a QuerySet
or anything else that goes fetch data and returns an iterable.
In my case in particular, I also added a TimedCache
so I don't read twice the
db to fill two dropdown with the same info in the same form:
class TimedCache: '''A function wrapper that caches the result for a while.''' def __init__(self, f, timeout): self.f = f self.timeout = timeout self.last_executed = None self.cache = None self.__name__ = f.__name__ + ' (Cached %ds)' % timeout def __call__(self): now = time.monotonic() if self.cache is None or (now - self.last_executed) > self.timeout: self.cache = self.f() self.last_executed = now return self.cache
I hope this helps someone.