Friday, July 5, 2013

How to Make Waffle Compatible with Django 1.5's Custom User Models

Waffle is a handy add-on for Django that allows fine-grain control over feature sets within an application.  At VM Farms, we use Waffle to control which users have access to specific features that are under development, but deployed within our live production environment.  Like I said, it’s handy!


Check out Waffle here.


Incidentally, our new portal is built on Django 1.5, and one of its new features is the ability to have fully customizable user models in django.contrib.auth.  Sweet!  This is great for us, as we are now able to move away from the static user model that existed within Django previously; however, despite Waffle’s best efforts to stay compatible, defining custom users can break Waffle.  Boo.


Read about the issue on Github here.


Until a fix is implemented, here is a breakdown of the workaround that we used to keep us moving forward.


The Heart of The Issue

Waffle makes the following four assumptions about user models:
  1. user.is_authenticated() method exists
  2. user.is_staff property exists
  3. user.is_superuser property exists
  4. user.groups.all() returns all Group objects attached to the user - making the assumption that you are using django.contrib.auth.group and linking it to the user model as a foreign key field

This clashes with Django 1.5’s minimum requirements for a custom user model (found here):

  1. have an integer primary key
  2. have a unique field for identification purposes (a “username”)
  3. provide a way to address the user in ‘short’ and ‘long’ forms

Some of Waffle’s requirements are satisfied if you go through the effort of making the user model compatible with django.contrib.admin, or by having the model inherit AbstractBaseUser. However, unless your model satisfies all four of Waffle’s assumptions, it will break the code. Luckily, there are easy ways to satisfy all requirements without having to run to your database to alter tables.



Faking is_authenticated()

It’s easy enough to fake results for is_authenticated():
class CustomUser(models.Model):
    ... 
    def is_authenticated(self):
        return True


Of course, if you actually plan on making use of is_authenticated() as a meaningful check, you’ll have to do more than just hard code a value to be returned.  Otherwise, it’s likely that you’re not interested in checking for “authenticated users only”, and just want it to work without throwing a 500.


Faking is_staff and is_superuser Model Fields

class CustomUser(models.Model):
    ... 
    @property
    def is_staff(self):
        # All admins are staff
        return self.is_admin

    @property
    def is_superuser(self):
        # All admins are superusers
        return self.is_admin


In this example, we already make use of a is_admin boolean field to track admins, so we get it to return that value in place of is_superuser and is_staff. You can just as easily hardcode it to return True/False if the field means nothing, and you just want Waffle to work.


Faking the Groups Many-to-Many Field

When it comes to the many-to-many Foreign Key field, we don’t actually need to fake a field - we just need to somehow make the call to [usermodel].groups.all() work.

We start by defining a fake property the same way we did above for is_superuser and is_staff.  However, this time we make it return a dummy object with an all() method under it:

class CustomUser(models.Model):
    ... 
    # Django Groups workaround for waffle
    @property
    def groups(self):
        class Groups():
            def __init__(self):
                pass
            def all(val):
                return None
        return Groups()


That way, user.groups.all() is now technically a perfectly valid command, and Waffle doesn’t complain.

Happy Waffling!

2 comments:

  1. Looks like you have an indentation issue in your last code example.

    ReplyDelete