Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Imports
3from django.conf import settings
4from django.db import models
5from django.utils.translation import ugettext_lazy as _
6import hashlib
8# Exports
10__all__ = (
11 "ProfileMixin",
12 "ProfileModel",
13 "ProfileSection",
14 "ProfileSectionItem",
15)
17# Constants
19AUTH_USER_MODEL = settings.AUTH_USER_MODEL
21LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL")
23STATIC_URL = getattr(settings, "STATIC_URL")
25# Descriptors
28class ProfileSection(object):
29 """Collects related profile fields together for display."""
31 def __init__(self, title):
32 self.items = list()
33 self.title = title
35 def add_item(self, label, value):
36 """Add an item to the section.
38 :param label: The label for the item, i.e. field's verbose name.
39 :type label: str
41 :param value: The value of the item.
43 :rtype: ProfileSectionItem
45 """
46 item = ProfileSectionItem(label, value)
47 self.items.append(item)
49 return item
52class ProfileSectionItem(object):
53 """An individual field within a section."""
55 def __init__(self, label, value):
56 self.label = label
57 self.value = value
59# Mixins
62class ProfileMixin(object):
63 """Establishes the API for user profiles."""
65 # class Meta:
66 # abstract = True
68 def __str__(self):
69 return self.get_full_name()
71 @property
72 def date_joined(self):
73 # noinspection PyUnresolvedReferences
74 return self.user.date_joined
76 @property
77 def email(self):
78 """Aliased for the user's email address."""
79 # noinspection PyUnresolvedReferences
80 return self.user.email
82 @classmethod
83 def for_user(cls, user, create=False, profile_name="profile"):
84 """Get (or create) the profile for a given user.
86 :param user: The user.
87 :type user: AUTH_USER_MODEL
89 :param create: Indicates whether a profile should be created if one does not already exist.
90 :type create: bool
92 :param profile_name: The related name of the profile field.
93 :type profile_name: str
95 :rtype: instance
96 :returns: An instance of a profile model for the user.
98 """
99 # There's no use in trying if the user is anonymous.
100 if not user.is_authenticated:
101 return None
103 # It is possible that the profile is already available on the user object. Try that before executing the rest of
104 # the code. But does this actually save a query?
105 # noinspection PyUnresolvedReferences
106 try:
107 return getattr(user, profile_name)
108 except (AttributeError, cls.DoesNotExist):
109 pass
111 # noinspection PyUnresolvedReferences
112 try:
113 # noinspection PyUnresolvedReferences
114 return cls.objects.get(user=user)
115 except cls.DoesNotExist:
116 pass
118 if create:
119 # noinspection PyUnresolvedReferences
120 profile, created = cls.objects.get_or_create(user=user)
121 return profile
123 return None
125 @property
126 def full_name(self):
127 """Aliased for ``get_full_name()``."""
128 return self.get_full_name()
130 @property
131 def full_name_with_honorifics(self):
132 """Alias of ``get_full_name_with_honorifics()``."""
133 return self.get_full_name_with_honorifics()
135 def get_form_class(self, request=None):
136 """Get the form class to use for editing the profile.
138 :param request: The current request instance.
139 :type request: HttpRequest
141 """
142 raise NotImplementedError()
144 def get_full_name(self):
145 """Get the full name of the user, or the user name if a full name is not available.
147 :rtype: str
149 """
150 raise NotImplementedError()
152 def get_full_name_with_honorifics(self):
153 """Get the full name of the user, including prefix and suffix, if available.
155 :rtype: str
157 """
158 raise NotImplementedError()
160 # noinspection PyMethodMayBeStatic
161 def get_redirect_url(self):
162 """Get the URL to which the user prefers to be redirected after logging in.
164 :rtype: str
166 .. note::
167 By default, the ``settings.LOGIN_REDIRECT_URL`` is returned. The profile model may implement a user
168 preference for the redirect.
170 """
171 return LOGIN_REDIRECT_URL
173 def get_thumbnail(self):
174 """Get the thumbnail URL for the user's avatar.
176 :rtype: str
178 """
179 return self._get_gravatar_url()
181 def get_sections(self):
182 """Get the sections to display for profile detail.
184 :rtype: list[ProfileSection]
186 """
187 raise NotImplementedError()
189 # noinspection PyMethodMayBeStatic
190 def get_time_zone(self):
191 """Get the timezone of the user. If unavailable, the default time zone should be returned.
193 :rtype: str
195 .. note::
196 The profile model that uses this mixin should implement a field (or other means) of establishing the user's
197 time zone (using geo-location for example), defaulting to ``settings.TIME_ZONE`` only if a time zone
198 preference is unavailable.
200 """
201 return settings.TIME_ZONE
203 @property
204 def has_thumbnail(self):
205 """Indicates the profile has a thumbnail image.
207 :rtype bool
209 """
210 return True
212 @property
213 def last_login(self):
214 """Alias for the user's last login."""
215 # noinspection PyUnresolvedReferences
216 return self.user.last_login
218 @property
219 def redirect_url(self):
220 """Alias for ``get_redirect_url()``."""
221 return self.get_redirect_url()
223 @property
224 def sections(self):
225 """Alias for ``get_sections()``."""
226 return self.get_sections()
228 @property
229 def thumbnail(self):
230 """Aliased for ``get_thumbnail()``."""
231 return self.get_thumbnail()
233 @property
234 def username(self):
235 """Aliased for the user's ``username``."""
236 # noinspection PyUnresolvedReferences
237 return self.user.username
239 def _get_gravatar_url(self):
240 """Get the user's gravatar.com URL.
242 :rtype: str
244 """
245 encoded_email = self.email.strip().lower().encode("utf-8")
246 email_hash = hashlib.md5(encoded_email).hexdigest()
248 # IDEA: The gavatar could be cached locally to save a URL call.
249 return "https://secure.gravatar.com/avatar/%s.jpg" % email_hash
252class ProfileModel(ProfileMixin, models.Model):
253 """An implementation of the profile mixin with sensible defaults.
255 .. note::
256 No fields with foreign keys are included.
258 This model extends the mixin to establishe some common profile attributes:
260 .. code-block:: python
262 from superdjango.contrib.accounts.profiles.models import ProfileModel
264 class Profile(ProfileModel):
266 user = models.OneToOneField(
267 AUTH_USER_MODEL,
268 help_text=_("The user account to which this profile is attached."),
269 related_name="profile",
270 verbose_name=_("user")
271 )
273 class Meta:
274 verbose_name = _("User Profile")
275 verbose_name_plural = _("User Profiles")
277 """
279 address = models.TextField(
280 _("address"),
281 blank=True,
282 help_text=_("Enter your mailing address."),
283 null=True
284 )
286 first_name = models.CharField(
287 _("first name"),
288 blank=True,
289 help_text=_("Your given name."),
290 max_length=128
291 )
293 gravatar_enabled = models.BooleanField(
294 _("enable gravatar"),
295 default=True,
296 help_text=_("Use gravatar.com for your profile picture.")
297 )
299 last_name = models.CharField(
300 _("last name"),
301 blank=True,
302 help_text=_("Your family name."),
303 max_length=128
304 )
306 middle_name = models.CharField(
307 _("middle name"),
308 blank=True,
309 help_text=_("Your middle name or initial."),
310 max_length=128,
311 null=True
312 )
314 phone_number = models.CharField(
315 _("phone number"),
316 blank=True,
317 help_text=_("Enter your phone number."),
318 max_length=128
319 )
321 photo = models.FileField(
322 _("photo"),
323 blank=True,
324 help_text=_("Upload a photo to use for your profile picture."),
325 null=True
326 )
328 prefix = models.CharField(
329 _("prefix"),
330 blank=True,
331 help_text=_("Enter a prefix to appear before your name or leave blank for nothing."),
332 max_length=64
333 )
335 suffix = models.CharField(
336 _("suffix"),
337 blank=True,
338 help_text=_("Enter a suffix to appear after your name or leave blank for nothing."),
339 max_length=64
340 )
342 # redirect_to choices must be established by the project.
343 redirect_to = models.CharField(
344 _("redirect to"),
345 blank=True,
346 help_text=_("By default, go to this page after logging in."),
347 max_length=256,
348 )
350 # time_zone choices must be established by the project.
351 time_zone = models.CharField(
352 _("time zone"),
353 blank=True,
354 help_text=_("Select the timezone in which you reside or work."),
355 max_length=128
356 )
358 class Meta:
359 abstract = True
361 def get_form_class(self, request=None):
362 from .forms import ProfileForm
363 return ProfileForm
365 def get_full_name(self):
366 a = list()
367 a.append(self.first_name)
369 if self.middle_name:
370 a.append(self.middle_name)
372 a.append(self.last_name)
374 if len(a) == 0:
375 a.append(self.username)
377 return " ".join(a)
379 def get_full_name_with_honorifics(self):
380 a = list()
382 if self.prefix:
383 a.append(self.prefix)
385 a.append(self.get_full_name())
387 if self.suffix:
388 a.append(self.suffix)
390 return " ".join(a)
392 def get_redirect_url(self):
393 """Get the user's redirect choice or the ``LOGIN_REDIRECT_URL`` if no choice has been made.
395 :rtype: str
397 """
398 return self.redirect_to or LOGIN_REDIRECT_URL
400 def get_sections(self):
401 """Get profile sections."""
402 a = list()
404 contact = ProfileSection(_("Contact"))
405 contact.add_item(_("Name"), self.get_full_name_with_honorifics())
406 contact.add_item(_("Email"), self.email)
407 contact.add_item(_("Phone"), self.phone_number)
408 contact.add_item(_("Address"), self.address)
409 a.append(contact)
411 user = ProfileSection(_("User"))
412 user.add_item(_("User Name"), self.username)
413 user.add_item(_("Since"), self.date_joined)
414 a.append(user)
416 prefs = ProfileSection(_("Preferences"))
417 prefs.add_item(_("Redirect To"), self.get_redirect_url())
418 prefs.add_item(_("Gravatar Enabled"), _("Yes") if self.gravatar_enabled else _("No"))
419 prefs.add_item(_("Time Zone"), self.get_time_zone())
420 a.append(prefs)
422 return a
424 def get_thumbnail(self):
425 """Uses gravatar or an uploaded photo."""
426 if self.gravatar_enabled:
427 return self._get_gravatar_url()
429 return self.photo.url
431 def get_time_zone(self):
432 """Get the time zone of the user or the default timezone.
434 :rtype: str
436 """
437 return self.time_zone or settings.TIME_ZONE
439 @property
440 def has_thumbnail(self):
441 """Determines a gravatar or photo is available."""
442 if self.gravatar_enabled:
443 return True
445 if self.photo:
446 return True
448 return False
450 @property
451 def title(self):
452 """Automated object title for UI views."""
453 return self.full_name