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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
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.
#
As long as you don't access any profile field there is no performance penalty.
#
Please login first before commenting.