Access transparently the user profile information

  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. Full Model History by willhardy 5 years, 4 months ago
  2. notify admin what fields have changed in form submission by pjv 3 years, 4 months ago
  3. Multiple User subclasses custom Auth backend by ungenio41 2 years, 7 months ago
  4. FieldAccessForm (per-field user access for forms derived from models) by Killarny 5 years, 6 months ago
  5. Model inheritance with content type and inheritance-aware manager by dan90 5 years, 7 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.

#

(Forgotten your password?)