4
votes

I'm trying to learn Django and I would like feedback from anyone who has any MVC/MTV/PHP/Ruby framework experience. Does anyone find that the user model is too tightly coupled with auth?

Background: When you first implement authentication for Django, you include the module django.contrib.auth

This will bring in several models like User, Group, Message etc. Let's focus on the User model as this is the one of the most important tables in any website.

In short the User table has these fields

User

  • username max_length 30, unique, [letters, digits, underscores]
  • password max_length 75
  • email max_length 75
  • ...and about 8 other useful fields like first_name, last_name, etc.

Goal:

I want to remove username and use email as the login for every user. It's a pretty simple request that many websites use these days.

I don't want to monkey patch the core code since this will make upgrading more difficult later on. This means modifying the User model is out of the question. I only want to do a few simple and basic things I expect a few frameworks to do so let me address how Django does it.

Adding new fields to the User model

Django docs says to use create another table and insert the fields there. You will have a one to one relationship between the User table and the Profile table. eg. If You want to add an image field to each user you add it to the profile table. A join query is made every single time. They've even specified a constant to tell the framework what table to use: AUTH_PROFILE_MODULE = 'accounts.UserProfile' I don't think it's the best practice to have to do a join query every time I want a field that should belong to the user table.

Another option is to use the function add_to_class. The django community has stated it's not good to define new fields outside of the main class because other developers who add methods won't know all the data members.

Editing old fields

The auth module does a check against two fields username and the hashed password. Looking at the above table I would need to change the username model to accept these properties. Length of 75 with all the valid characters of the email. The django suggests I check against the email field.

Two problems arise if I use the email field to auth against: I need to write a new class to be used in a constant AUTHENTICATION_BACKEND, so it checks against the email field and I have an unused field called username.

Adding new methods

In MVC/MTV a design principle is to use fat models skinny controllers. Since the model is declared in auth, I'm not sure how one is supposed to add methods that act on the user model's fields. Since django suggests using a Profile model, I suppose they will have to go there.

Extending the User class

A small annoyance would be that I can't use the name 'User' and instead must use 'Users' or 'Accounts'. A bigger one is I don't think the auth would recognize this new module. Meaning I would have to rewrite a bunch functionality that is is present. This one doesn't bother me as it's something I expect to do in other frameworks.

Any comments are appreciated. I wouldn't ask all these questions and look for solutions if I wasn't truly interested in using django.

3
Django cache user profile, so it will execute join query for the first profile access only.gorsky

3 Answers

4
votes

I agree that django's incessant clinginess to the auth models is absurd. My job requires me to create ultra scalable and very high load sites which sometimes require user authentication and djano's auth model + permissions does not fit with that.

Fortunately, it's not difficult to replace.

First, create a custom User model.

class User(models.Model):
    ...fields...
    #Define some interface methods to be compatible.
    def get_and_delete_messages(self):
    def is_active(self):
    def is_anonymous(self):
    def is_authenticated(self):
    def is_staff(self):
    def has_perm(self, perm_list):

Second, create your own authentication back-end.

class LocalAccount(object):
    """
    This checks our local user DB for authentication
    """
    def authenticate(self, username=None, password=None):
        try:
            user = User.objects.get(alias=username)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.select_related().get(pk=user_id)
        except User.DoesNotExist:
            return None

#settings.py
AUTHENTICATION_BACKENDS = (
    'helpers.auth.LocalAccount',
)

That should solve most of your issues, I don't even think all of the methods you would find on django.contrib.auth.User are necessary, I recommend trying it out.

The one gotcha here is that the admin may start to bitch, fortunately that's really easy to patch using simple python inheritance as well. That's another question though :)

1
votes

At the end of the day your project's auth backend needs some sort of store for auth credentials. That the default auth backend is tightly coupled to the User model is not strange in this respect. It's easy enough to substitute your own definition for the user model if you write your own auth backend, as I have in the past.

0
votes

I created my Profile model and use AUTH_PROFILE_MODULE, so I have complete control over my model, I can modify fields, add methods, etc. Now I'm thinking about using cache and writing middleware that will get profile from cache if possible.

To login using email you could write very simple auth backend:

from django.contrib.auth.models import User
from django.contrib.auth.backends import ModelBackend

class EmailModelBackend(ModelBackend):
    def authenticate(self, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None