views:

184

answers:

3

I have a data model with a bitfield defined something like this:

alter table MemberFlags add column title varchar(50) not null default '';
alter table MemberFlags add column value integer( 3) not null default 0;

insert into MemberFlags (title, value) values
    ("Blacklisted",             1),
    ("Special Guest",           2),
    ("Attend Ad-hoc Sessions",  4),
    ("Attend VIP Sessions",     8),
    ("Access Facility A",      16),
    ("Access Facility B",      32)

And used like this:

alter table Membership add column title varchar(50) not null default '';
alter table Membership add column flags integer( 3) not null default 0;

insert into Membership (title, flags) values
    ("Guest Pass",          4+2 ),
    ("Silver Plan",    16+  4   ),
    ("Gold Plan",   32+16+  4+2 ),
    ("VIP Pass",    32+16+8+4+2 )

My questions are:

A) What's the easiest way to represent the different bitflags as separate items in the admin site? Should I override the template, or do something with forms?

B) How about the search list? I could create functions in the model to represent each bit, but how would searching and sorting be done?

I'm new to Django.

+2  A: 

I think the best solution here would be for you to create a new field type by subclassing models.Field. You could make use of the choices parameter to assign the valid bit flags and their meanings. This would help keep your model declaration clean and readable, with a final result along the lines of:

class BitFlagField(models.Field):

    ...

class MyModel(models.Model):

    ...

    FLAG_CHOICES = (
        (1, 'Blacklisted'),
        (2, 'Special Guest'),
        (4, 'Attend Ad-hoc Sessions'),
        (8, 'Attend VIP Sessions'),
        (16, 'Access Facility A'),
        (32, 'Access Facility B'),
    )
    flags = BitFlagField(choices=FLAG_CHOICES)

   ...

The Django documentation has a great in-depth article on how to go about subclassing models.Field:

Writing Custom Model Fields
It seems to cover everything you need to do, including:

If you're looking for an example of a subclassed field, this snippet might be of use. Its goal is similar (multiple choices as a model field), but its manner of storing them in the database differs (it's using a CSV text field instead of bit flags).

Andrew
+1  A: 

Working off the snippet in Andrew's answer, here are the changes you'd need to make:

from django.db import models
from django import forms

class BitFlagFormField(forms.MultipleChoiceField):
    widget = forms.CheckboxSelectMultiple

    def __init__(self, *args, **kwargs):
        super(BitFlagFormField, self).__init__(*args, **kwargs)

class BitFlagField(models.Field):
    __metaclass__ = models.SubfieldBase

    def get_internal_type(self):
        return "Integer"

    def get_choices_default(self):
        return self.get_choices(include_blank=False)

    def _get_FIELD_display(self, field):
        value = getattr(self, field.attname)
        choicedict = dict(field.choices)

    def formfield(self, **kwargs):
        # don't call super, as that overrides default widget if it has choices
        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 
                    'help_text': self.help_text, 'choices':self.choices}
        if self.has_default():
            defaults['initial'] = self.get_default()
        defaults.update(kwargs)
        return BitFlagFormField(**defaults)

    def get_db_prep_value(self, value):
        if isinstance(value, int):
            return value
        elif isinstance(value, list):
            return sum(value)

    def to_python(self, value):
        result = []
        n = 1
        while value > 0:
            if (value % 2) > 0:
                result.append(n)
            n *= 2
            value /= 2
        return sorted(result)


    def contribute_to_class(self, cls, name):
        super(BitFlagField, self).contribute_to_class(cls, name)
        if self.choices:
            func = lambda self, fieldname = name, choicedict = dict(self.choices):" and ".join([choicedict.get(value,value) for value in getattr(self,fieldname)])
            setattr(cls, 'get_%s_display' % self.name, func)
Jordan Reiter
You'd still need to write the `get_prep_lookup` function; one way to implement this would be to just loop through each item in the search, and for each item loop through `self.choices` and if the search term is contained in the choice, then add the value for that choice to the lookup key. So, for example, your search might be "Access Facility A, Special Guest" so it would find each choice and add them up, and return `18`.
Jordan Reiter
I like it. Haven't had time to try it yet though. Thanks for the tips on searching and filtering. What would I need to do to get separate columns in the admin list view?
no
A: 

This is how I would use the flags with my User class:

FLAGS = {
    1:"Blacklisted",
    2:"SpecialGuest",
    4:"AttendAd-hocSessions",
    8:"AttendVIPSessions",
    16:"AccessFacilityA",
    32:"AccessFacilityB",
}

class User(object):
    def __init__(self, name="John Doe", groups=0):
        self.name = name
        self.groups = groups
    def memberof(self):
        ''' Display string representation of the groups. '''
        for flag in sorted(FLAGS):
            if (flag & self.groups) == flag:
                print FLAGS[flag]

Of course instead of printing the flags, you can create a comma-separated string to display in the admin view, or whatever you desire.

For the admin, just use a boolean for each of the group values.

Wayne Werner