hobbes3
hobbes3

Reputation: 30238

Check for multiple values in a M2M relationship in Django

In English, I want to check whether a user's type is either a viewer, a moderator, or an administrator.

Here is the relevant part of my models.py

class UserType( models.Model ) :
    name = models.CharField( max_length = 135 )

    # id |       name
    #----+------------------
    #  1 | tenant
    #  2 | property manager
    #  3 | property owner
    #  4 | vendor manager
    #  5 | vendor
    #  6 | viewer
    #  7 | moderator
    #  8 | administrator

class UserProfile( models.Model ) :
    user       = models.OneToOneField( User )
    user_types = models.ManyToManyField( UserType, null = True, blank = True )

By the way, I set user.profile to be the same thing as user.get_profile().

In my views.py code, I want to do the checking. I figured out that

[ user_type.pk for user_type in user.profile.user_types.all() ]

will give me a list of pk's for the user's user_type, like so [ 1, 2, 6 ]. Meaning that this particular user is a tenant (1), property manager (2), and a viewer (6).

If I just want to check one user_type, then I can simply do

if 6 in [ user_type.pk for user_type in user.profile.user_types.all() ] :
    # This user is a viewer

But how can I check multiple user_types/pks? I wanted to do something like

# This won't work
if [ 6, 7, 8 ] in [ user_type.pk for user_type in user.profile.user_types.all() ] :
    # This user is either a viewer, moderator, or administrator

Also is my method of list comprehension to check user_types the Django-way? It doesn't seem like it, but I couldn't figure out how to query it cleanly in Django.

Any tips and suggestions welcomed. Thanks in advance!

EDIT:

I just figured out I can list pks the more Django-way with values_list

user_types = user.profile.user_types.values_list( 'pk', flat = True )
# [ 1, 2, 6 ]

I also figured out that I could check multiple values like this

if len( set( [ 1, 9 ] ).intersection( set( user_types ) ) )
    # True because of 1 is in user_types (don't care about 9)

if len( set( [ 4, 99 ] ).intersection( set( user_types ) ) )
    # False because 4 nor 99 is in user_types

But even this set method doesn't seem very Django-friendly. There must be an easier way right?

Upvotes: 0

Views: 417

Answers (1)

enticedwanderer
enticedwanderer

Reputation: 4346

Simply do:

def has_roles(user, roles):
    return user.profile.user_types.filter(pk__in=roles).count() == len(roles)

print has_roles(user, [6,7,8])

P.S. I would shy away from using hard coded PK ID numbers as your identifiers. Too many things can go wrong with that. Instead define a mapping at runtime and reference them by names. Multiple things will improve:

  • Your code will be much easier to read
  • Django will offload model instance matching to the DB the first item
  • From then on you can cache the ID and not ask the DB again

Then you can do:

class UserType( models.Model ):
    TYPES = (( 'tenant', 'Tenant'),
             ( 'propman', 'Property Manager'),
             ( 'propown', 'Property Owner'),
             ( 'vendman', 'Vendor Manager'),
             ( 'vendor', 'Vendor'),
             ( 'viewer', 'Viewer'),
             ( 'moderator', 'Moderator'),
             ( 'admin', 'Administrator'))

    name = models.CharField( max_length = 135, choices=TYPES )

def has_role(user, role):
    return user.profile.user_types.filter(name=role).count() == 1

def has_roles(user, roles):
    return user.profile.user_types.filter(name__in=roles).count() == len(roles)

print has_roles(user, ['viewer','moderator','admin'])

Finally, you can add the two functions above to:

class UserProfile( models.Model ) :
    user       = models.OneToOneField( User )
    user_types = models.ManyToManyField( UserType, null = True, blank = True )


    def has_role(self, role):
        return self.user_types.filter(name=role).count() == 1

    def has_roles(self, roles):
        return self.user_types.filter(name__in=roles).count() == len(roles)

And then use it like this in the future:

u = User.objects.get(username='me')
if u.userprofile.has_role('admin'):
    print 'I have the powah!'

Upvotes: 3

Related Questions