Login

Access transparently the user profile information

Author:
gsakkis
Posted:
September 4, 2010
Language:
Python
Version:
1.2
Score:
2 (after 2 ratings)

Splitting the information about a user across two models (User and UserProfile) is not to everyone's liking. This snippet monkeypatches the User model so that users can access transparently their profile information while still storing the data in two tables under the hood. Thus it is similar to the inheritance approach (http://scottbarnham.com/blog/2008/08/21/extending-the-django-user-model-with-inheritance/) but has the benefit of (a) not requiring a custom authentication backend or middleware and (b) loading the profile instance lazily, so there's no extra overhead if the profile infromation is not accessed. Read the docstrings for more details.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
from django.conf import settings
from django.contrib.auth.models import User, SiteProfileNotAvailable
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.signals import connection_created
from django.db.models import DateField, DateTimeField, get_model
from django.db.models.signals import post_save
from django.utils.functional import curry

__all__ = ['inject_user_profile']


def inject_user_profile(include=None, exclude=None):
    '''Allow users to access transparently their profile information.
    
    The ``User`` model is monkeypatched so that the fields of the profile model
    (specified in ``settings.AUTH_PROFILE_MODULE``) can be accessed (both get
    and set) transparently from a user instance. The associated profile is
    loaded lazily the first time a field is accessed; therefore there is no
    overhead if the profile information is never accessed. Once the profile has
    been fetched, saving the user instance also saves the profile.
    
    By default, all profile fields are added as ``User`` properties, with the
    exception of ``user`` and those fields having the same name with an existing
    User attribute. The optional ``include`` or ``exclude`` parameter can be used
    to limit further the exposed profile fields.
    
    For consistency with regular models, ``User`` is also patched with the
    ``get_{FIELD}_display`` method for every ``FIELD`` that has ``choices`` and
    with the ``get_next_by_{FIELD}``/``get_previous_by_{FIELD}`` methods for every
    ``FIELD`` that is ``DateField`` or ``DateTimeField`` and has ``null=False``.
    
    The monkeypatching is delayed until the first database connection is initiated.
    Therefore it is not necessary for the ``settings.AUTH_PROFILE_MODULE`` model
    to be known when this function is called; it can be called from almost
    anywhere, e.g. in the project's ``__init__.py``.
     
    :param include: If not ``None``, an iterable of profile field names to add
        as ``User`` properties (``exclude`` must be ``None``).
    :param exclude: If not ``None``, an iterable of profile field names to not
        add as ``User`` properties (``include`` must be ``None``).
    '''
    
    def monkeypatch_user(sender, **kwargs):
        profile_model = get_profile_model()
        exclude_names = set(field.name for field in User._meta.fields)
        exclude_names.add('user')
        if include is not None:
            if exclude is not None:
                raise ValueError('Cannot pass both "include" and "exclude"')
            include_names = set(include) - exclude_names
            iterfields = (field for field in profile_model._meta.fields
                          if field.name in include_names)
        else:
            if exclude:
                exclude_names.update(exclude)
            iterfields = (field for field in profile_model._meta.fields
                          if field.name not in exclude_names)
        for field in iterfields:
            name = field.name    
            setattr_if_unset(User, name, _make_profile_property(name))
            if field.choices:
                setattr_if_unset(User, 'get_%s_display' % name,
                                 curry(User._get_FIELD_display, field=field))
            if isinstance(field, (DateField, DateTimeField)) and not field.null:
                setattr_if_unset(User, 'get_next_by_%s' % name,
                                 curry(_get_next_or_previous_by_profile_FIELD,
                                       field=field, is_next=True))
                setattr_if_unset(User, 'get_previous_by_%s' % name,
                                 curry(_get_next_or_previous_by_profile_FIELD,
                                       field=field, is_next=False))
        post_save.connect(_save_profile_listener, sender=User)
        connection_created.disconnect(monkeypatch_user)
    connection_created.connect(monkeypatch_user, weak=False)


def get_profile_model():
    if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
        raise SiteProfileNotAvailable('You need to set AUTH_PROFILE_MODULE in '
                                      'your project settings')
    try:
        app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
    except ValueError:
        raise SiteProfileNotAvailable('app_label and model_name should'
            ' be separated by a dot in the AUTH_PROFILE_MODULE setting')
    try:
        model = get_model(app_label, model_name)
        if model is None:
            raise SiteProfileNotAvailable('Unable to load the profile model, '
                        'check AUTH_PROFILE_MODULE in your project settings')
        return model
    except (ImportError, ImproperlyConfigured):
        raise SiteProfileNotAvailable


def setattr_if_unset(obj, name, value):
    if not hasattr(obj, name):
        setattr(obj, name, value)

def _make_profile_property(name):
    return property(lambda self: getattr(self.get_profile(), name),
                    lambda self, value: setattr(self.get_profile(), name, value))

def _get_next_or_previous_by_profile_FIELD(self, field, is_next):
    return self.get_profile()._get_next_or_previous_by_FIELD(field, is_next).user

def _save_profile_listener(sender, instance, created, **kwargs):    
    if hasattr(instance, '_profile_cache'):
        instance._profile_cache.save()

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 4 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

peterbe (on September 8, 2010):

Nice snippet but not big fan of the approach.

What about speed? I often access the User model which only has a few fields in its SELECT whereas some of my UserProfile models can be huge.

#

gsakkis (on September 9, 2010):

As long as you don't access any profile field there is no performance penalty.

#

Please login first before commenting.