# Imports
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
import hashlib
# Exports
__all__ = (
"ProfileMixin",
"ProfileModel",
"ProfileSection",
"ProfileSectionItem",
)
# Constants
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL")
STATIC_URL = getattr(settings, "STATIC_URL")
# Descriptors
[docs]class ProfileSection(object):
"""Collects related profile fields together for display."""
[docs] def __init__(self, title):
self.items = list()
self.title = title
[docs] def add_item(self, label, value):
"""Add an item to the section.
:param label: The label for the item, i.e. field's verbose name.
:type label: str
:param value: The value of the item.
:rtype: ProfileSectionItem
"""
item = ProfileSectionItem(label, value)
self.items.append(item)
return item
[docs]class ProfileSectionItem(object):
"""An individual field within a section."""
[docs] def __init__(self, label, value):
self.label = label
self.value = value
# Mixins
[docs]class ProfileMixin(object):
"""Establishes the API for user profiles."""
# class Meta:
# abstract = True
def __str__(self):
return self.get_full_name()
@property
def date_joined(self):
# noinspection PyUnresolvedReferences
return self.user.date_joined
@property
def email(self):
"""Aliased for the user's email address."""
# noinspection PyUnresolvedReferences
return self.user.email
[docs] @classmethod
def for_user(cls, user, create=False, profile_name="profile"):
"""Get (or create) the profile for a given user.
:param user: The user.
:type user: AUTH_USER_MODEL
:param create: Indicates whether a profile should be created if one does not already exist.
:type create: bool
:param profile_name: The related name of the profile field.
:type profile_name: str
:rtype: instance
:returns: An instance of a profile model for the user.
"""
# There's no use in trying if the user is anonymous.
if not user.is_authenticated:
return None
# It is possible that the profile is already available on the user object. Try that before executing the rest of
# the code. But does this actually save a query?
# noinspection PyUnresolvedReferences
try:
return getattr(user, profile_name)
except (AttributeError, cls.DoesNotExist):
pass
# noinspection PyUnresolvedReferences
try:
# noinspection PyUnresolvedReferences
return cls.objects.get(user=user)
except cls.DoesNotExist:
pass
if create:
# noinspection PyUnresolvedReferences
profile, created = cls.objects.get_or_create(user=user)
return profile
return None
@property
def full_name(self):
"""Aliased for ``get_full_name()``."""
return self.get_full_name()
@property
def full_name_with_honorifics(self):
"""Alias of ``get_full_name_with_honorifics()``."""
return self.get_full_name_with_honorifics()
[docs] def get_full_name(self):
"""Get the full name of the user, or the user name if a full name is not available.
:rtype: str
"""
raise NotImplementedError()
[docs] def get_full_name_with_honorifics(self):
"""Get the full name of the user, including prefix and suffix, if available.
:rtype: str
"""
raise NotImplementedError()
# noinspection PyMethodMayBeStatic
[docs] def get_redirect_url(self):
"""Get the URL to which the user prefers to be redirected after logging in.
:rtype: str
.. note::
By default, the ``settings.LOGIN_REDIRECT_URL`` is returned. The profile model may implement a user
preference for the redirect.
"""
return LOGIN_REDIRECT_URL
[docs] def get_thumbnail(self):
"""Get the thumbnail URL for the user's avatar.
:rtype: str
"""
return self._get_gravatar_url()
[docs] def get_sections(self):
"""Get the sections to display for profile detail.
:rtype: list[ProfileSection]
"""
raise NotImplementedError()
# noinspection PyMethodMayBeStatic
[docs] def get_time_zone(self):
"""Get the timezone of the user. If unavailable, the default time zone should be returned.
:rtype: str
.. note::
The profile model that uses this mixin should implement a field (or other means) of establishing the user's
time zone (using geo-location for example), defaulting to ``settings.TIME_ZONE`` only if a time zone
preference is unavailable.
"""
return settings.TIME_ZONE
@property
def has_thumbnail(self):
"""Indicates the profile has a thumbnail image.
:rtype bool
"""
return True
@property
def last_login(self):
"""Alias for the user's last login."""
# noinspection PyUnresolvedReferences
return self.user.last_login
@property
def redirect_url(self):
"""Alias for ``get_redirect_url()``."""
return self.get_redirect_url()
@property
def sections(self):
"""Alias for ``get_sections()``."""
return self.get_sections()
@property
def thumbnail(self):
"""Aliased for ``get_thumbnail()``."""
return self.get_thumbnail()
@property
def username(self):
"""Aliased for the user's ``username``."""
# noinspection PyUnresolvedReferences
return self.user.username
def _get_gravatar_url(self):
"""Get the user's gravatar.com URL.
:rtype: str
"""
encoded_email = self.email.strip().lower().encode("utf-8")
email_hash = hashlib.md5(encoded_email).hexdigest()
# IDEA: The gavatar could be cached locally to save a URL call.
return "https://secure.gravatar.com/avatar/%s.jpg" % email_hash
[docs]class ProfileModel(ProfileMixin, models.Model):
"""An implementation of the profile mixin with sensible defaults.
.. note::
No fields with foreign keys are included.
This model extends the mixin to establishe some common profile attributes:
.. code-block:: python
from superdjango.contrib.accounts.profiles.models import ProfileModel
class Profile(ProfileModel):
user = models.OneToOneField(
AUTH_USER_MODEL,
help_text=_("The user account to which this profile is attached."),
related_name="profile",
verbose_name=_("user")
)
class Meta:
verbose_name = _("User Profile")
verbose_name_plural = _("User Profiles")
"""
address = models.TextField(
_("address"),
blank=True,
help_text=_("Enter your mailing address."),
null=True
)
first_name = models.CharField(
_("first name"),
blank=True,
help_text=_("Your given name."),
max_length=128
)
gravatar_enabled = models.BooleanField(
_("enable gravatar"),
default=True,
help_text=_("Use gravatar.com for your profile picture.")
)
last_name = models.CharField(
_("last name"),
blank=True,
help_text=_("Your family name."),
max_length=128
)
middle_name = models.CharField(
_("middle name"),
blank=True,
help_text=_("Your middle name or initial."),
max_length=128,
null=True
)
phone_number = models.CharField(
_("phone number"),
blank=True,
help_text=_("Enter your phone number."),
max_length=128
)
photo = models.FileField(
_("photo"),
blank=True,
help_text=_("Upload a photo to use for your profile picture."),
null=True
)
prefix = models.CharField(
_("prefix"),
blank=True,
help_text=_("Enter a prefix to appear before your name or leave blank for nothing."),
max_length=64
)
suffix = models.CharField(
_("suffix"),
blank=True,
help_text=_("Enter a suffix to appear after your name or leave blank for nothing."),
max_length=64
)
# redirect_to choices must be established by the project.
redirect_to = models.CharField(
_("redirect to"),
blank=True,
help_text=_("By default, go to this page after logging in."),
max_length=256,
)
# time_zone choices must be established by the project.
time_zone = models.CharField(
_("time zone"),
blank=True,
help_text=_("Select the timezone in which you reside or work."),
max_length=128
)
class Meta:
abstract = True
[docs] def get_full_name(self):
a = list()
a.append(self.first_name)
if self.middle_name:
a.append(self.middle_name)
a.append(self.last_name)
if len(a) == 0:
a.append(self.username)
return " ".join(a)
[docs] def get_full_name_with_honorifics(self):
a = list()
if self.prefix:
a.append(self.prefix)
a.append(self.get_full_name())
if self.suffix:
a.append(self.suffix)
return " ".join(a)
[docs] def get_redirect_url(self):
"""Get the user's redirect choice or the ``LOGIN_REDIRECT_URL`` if no choice has been made.
:rtype: str
"""
return self.redirect_to or LOGIN_REDIRECT_URL
[docs] def get_sections(self):
"""Get profile sections."""
a = list()
contact = ProfileSection(_("Contact"))
contact.add_item(_("Name"), self.get_full_name_with_honorifics())
contact.add_item(_("Email"), self.email)
contact.add_item(_("Phone"), self.phone_number)
contact.add_item(_("Address"), self.address)
a.append(contact)
user = ProfileSection(_("User"))
user.add_item(_("User Name"), self.username)
user.add_item(_("Since"), self.date_joined)
a.append(user)
prefs = ProfileSection(_("Preferences"))
prefs.add_item(_("Redirect To"), self.get_redirect_url())
prefs.add_item(_("Gravatar Enabled"), _("Yes") if self.gravatar_enabled else _("No"))
prefs.add_item(_("Time Zone"), self.get_time_zone())
a.append(prefs)
return a
[docs] def get_thumbnail(self):
"""Uses gravatar or an uploaded photo."""
if self.gravatar_enabled:
return self._get_gravatar_url()
return self.photo.url
[docs] def get_time_zone(self):
"""Get the time zone of the user or the default timezone.
:rtype: str
"""
return self.time_zone or settings.TIME_ZONE
@property
def has_thumbnail(self):
"""Determines a gravatar or photo is available."""
if self.gravatar_enabled:
return True
if self.photo:
return True
return False
@property
def title(self):
"""Automated object title for UI views."""
return self.full_name