10

I've been searching for an elegant way to represent a multi-select weekday field (Mon, Tues, Wed...) in a Django model. I was initially thinking of going integer field using bitwise math but I am not sure if this would be the way to go.

This would be a mostly-read field. I would want the Queryset method to be something like Entry.objects.get(weekdays__contains=MONDAY) Where MONDAY would be a constant.

Perhaps someone could come up with a better solution? Or maybe someone has done something similar and has some example code they could contribute?

Belmin Fernandez
  • 8,307
  • 9
  • 51
  • 75
  • Without further information (size of dataset, mostly read vs mostly write, etc.) going with bitfields feels like the dreaded Premature Optimization. – Peter Rowell Sep 08 '10 at 04:48
  • I will append additional information to the question. Thank you Mr. Rowell. – Belmin Fernandez Sep 08 '10 at 11:41
  • Have you considered adding a many to many relationship between a `Weekday` model and the model in question? I know this is a bit of an overkill considering that week days are fixed in number, but it would make filtering very straightforward. – Manoj Govindan Sep 08 '10 at 11:47
  • Although the filtering would be straightforward, seems particularly inefficient to me because of static data (weekday names) stored in a db table. However, for the time being, I might just stick with that since it's a simple implementation. – Belmin Fernandez Sep 08 '10 at 13:42

1 Answers1

16

This is an old question, but I thought I would show how it could be done reasonably simply in Django.

Here is a helper class for preparing your choices:

class BitChoices(object):
  def __init__(self, choices):
    self._choices = []
    self._lookup = {}
    for index, (key, val) in enumerate(choices):
      index = 2**index
      self._choices.append((index, val))
      self._lookup[key] = index

  def __iter__(self):
    return iter(self._choices)

  def __len__(self):
    return len(self._choices)

  def __getattr__(self, attr):
    try:
      return self._lookup[attr]
    except KeyError:
      raise AttributeError(attr)

  def get_selected_keys(self, selection):
    """ Return a list of keys for the given selection """
    return [ k for k,b in self._lookup.iteritems() if b & selection]

  def get_selected_values(self, selection):
    """ Return a list of values for the given selection """
    return [ v for b,v in self._choices if b & selection]

Define your model with a PositiveIntegerField, and the choices you would like:

WEEKDAYS = BitChoices((('mon', 'Monday'), ('tue', 'Tuesday'), ('wed', 'Wednesday'),
               ('thu', 'Thursday'), ('fri', 'Friday'), ('sat', 'Saturday'),
               ('sun', 'Sunday')
           ))

This means you can access the values like this:

>>> print list(WEEKDAYS)
[(1, 'Monday'), (2, 'Tuesday'), (4, 'Wednesday'), (8, 'Thursday'), (16, 'Friday'), (32, 'Saturday'), (64, 'Sunday')]
>>> print WEEKDAYS.fri
16
>>> print WEEKDAYS.get_selected_values(52)
['Wednesday', 'Friday', 'Saturday']

Now define your model with a PositiveIntegerField and these choices:

class Entry(models.Model):
    weekdays = models.PositiveIntegerField(choices=WEEKDAYS)

And your models are done. For queries, the following does the trick:

Entry.objects.extra(where=["weekdays & %s"], params=[WEEKDAYS.fri])

There may be a way to create a Q() object subclass that neatly packages queries, so they look like this:

Entry.objects.filter(HasBit('weekdays', WEEKDAYS.fri))

Or even hack at a F() subclass to create something like this:

Entry.objects.filter(weekdays=HasBit(WEEKDAYS.fri))

But I don't have the time to explore that at the moment. .where works fine and can be abstracted into a queryset function.

One final consideration is that you might light to make a custom model field that converts the bit mask in the database to a list or set in Python. You could then use a SelectMultiple widget (or CheckboxSelectMultiple) to allow the user to select their values in the admin.

Will Hardy
  • 14,588
  • 5
  • 44
  • 43
  • This answer http://stackoverflow.com/questions/19645227/django-create-multiselect-checkbox-input broaches that subject regarding teh widget – Yablargo Mar 04 '14 at 22:37
  • Just want to say thanks again, I was able to stumble through building a widget against your class, and it works like a champ. – Yablargo Mar 05 '14 at 16:18
  • @Yablargo and author: Thanks a lot! I also was able to make a widget against your class, but I'm stuck on making the reverse function (from (string)value (let's say "12") to datadict {(4, "wednesday"), (8, "thursday")} to show (and edit) initial data. Could you please help here: http://stackoverflow.com/q/25575951/3849359? Thanks a million! – gabn88 Sep 08 '14 at 11:06
  • I've got the source code in the office for my working version, I'll dig it up monday – Yablargo Sep 13 '14 at 02:39