src/ej_profiles/models.py
import hashlib
from boogie.fields import EnumField
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.staticfiles.storage import staticfiles_storage
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _, gettext as __
from rest_framework.authtoken.models import Token
from sidekick import delegate_to, import_later
from .enums import Race, Gender, STATE_CHOICES_MAP
from .utils import years_from
SocialAccount = import_later("allauth.socialaccount.models:SocialAccount")
User = get_user_model()
class Profile(models.Model):
"""
User profile
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
race = EnumField(Race, _("Race"), default=Race.NOT_FILLED)
ethnicity = models.CharField(_("Ethnicity"), blank=True, max_length=50)
education = models.CharField(_("Education"), blank=True, max_length=140)
gender = EnumField(Gender, _("Gender identity"), default=Gender.NOT_FILLED)
gender_other = models.CharField(_("User provided gender"), max_length=50, blank=True)
birth_date = models.DateField(_("Birth Date"), null=True, blank=True)
country = models.CharField(_("Country"), blank=True, max_length=50)
state = models.CharField(_("State"), blank=True, max_length=3)
city = models.CharField(_("City"), blank=True, max_length=140)
biography = models.TextField(_("Biography"), blank=True)
occupation = models.CharField(_("Occupation"), blank=True, max_length=50)
political_activity = models.TextField(_("Political activity"), blank=True)
profile_photo = models.ImageField(_("Profile Photo"), blank=True, null=True, upload_to="profile_images")
phone_number = models.CharField(_("Phone number"), blank=True, max_length=11)
completed_tour = models.BooleanField(default=False, blank=True, null=True)
name = delegate_to("user")
email = delegate_to("user")
is_active = delegate_to("user")
is_staff = delegate_to("user")
is_superuser = delegate_to("user")
limit_board_conversations = delegate_to("user")
@property
def age(self):
return None if self.birth_date is None else years_from(self.birth_date)
class Meta:
ordering = ["user__email"]
def __str__(self):
return __("{name}'s profile").format(name=self.user.name)
def __getattr__(self, attr):
try:
user = self.user
except User.DoesNotExist:
raise AttributeError(attr)
return getattr(user, attr)
@property
def gender_description(self):
if self.gender != Gender.NOT_FILLED:
return self.gender.description
return self.gender_other
@property
def token(self):
token = Token.objects.get_or_create(user_id=self.id)
return token[0].key
@property
def image_url(self):
try:
return self.profile_photo.url
except ValueError:
if apps.is_installed("allauth.socialaccount"):
for account in SocialAccount.objects.filter(user=self.user):
picture = account.get_avatar_url()
return picture
return staticfiles_storage.url("/img/icons/navbar_profile.svg")
@property
def has_image(self):
return bool(
self.profile_photo
or (
apps.is_installed("allauth.socialaccount") and SocialAccount.objects.filter(user_id=self.id)
)
)
@property
def is_filled(self):
fields = (
"race",
"age",
"birth_date",
"education",
"ethnicity",
"country",
"state",
"city",
"biography",
"occupation",
"political_activity",
"has_image",
"gender_description",
"phone_number",
)
return bool(all(getattr(self, field) for field in fields))
def get_absolute_url(self):
return reverse("user-detail", kwargs={"pk": self.id})
def profile_fields(self, user_fields=False, blacklist=None):
"""
Return a list of tuples of (field_description, field_value) for all
registered profile fields.
"""
fields = [
"city",
"state",
"country",
"occupation",
"education",
"ethnicity",
"gender",
"race",
"political_activity",
"biography",
"phone_number",
]
field_map = {field.name: field for field in self._meta.fields}
null_values = {Gender.NOT_FILLED, Race.NOT_FILLED, None, ""}
# Create a tuples of (attribute, human-readable name, value)
triple_list = []
for field in fields:
description = field_map[field].verbose_name
value = getattr(self, field)
if value in null_values:
value = None
elif hasattr(self, f"get_{field}_display"):
value = getattr(self, f"get_{field}_display")()
triple_list.append((field, description, value))
# Age is not a real field, but a property. We insert it after occupation
triple_list.insert(3, ("age", _("Age"), self.age))
# Add fields in the user profile (e.g., e-mail)
if user_fields:
triple_list.insert(0, ("email", _("E-mail"), self.user.email))
# Prepare blacklist of fields and overrides
if blacklist is None:
blacklist = settings.EJ_PROFILE_EXCLUDE_FIELDS
name_overrides = getattr(settings, "EJ_PROFILE_FIELD_NAMES", {})
return list(prepare_fields(triple_list, blacklist, name_overrides))
def statistics(self):
"""
Return a dictionary with all profile statistics.
"""
return dict(
votes=self.user.votes.count(),
comments=self.user.comments.count(),
conversations=self.user.conversations.count(),
)
def badges(self):
"""
Return all profile badges.
"""
return self.user.badges_earned.all()
def comments(self):
"""
Return all profile comments.
"""
return self.user.comments.all()
def role(self):
"""
A human-friendly description of the user role in the platform.
"""
if self.user.is_superuser:
return _("Root")
if self.user.is_staff:
return _("Administrative user")
return _("Regular user")
def get_state_display(self):
return STATE_CHOICES_MAP.get(self.state, self.state) or _("(Not Filled)")
def prepare_fields(triples, blacklist, overrides):
for a, b, c in triples:
if a in blacklist:
continue
b = overrides.get(a, b)
yield b, c
def gravatar_fallback(id_):
"""
Computes gravatar fallback image URL from a unique string identifier
"""
digest = hashlib.md5(id_.encode("utf-8")).hexdigest()
return "https://gravatar.com/avatar/{}?s=40&d=mm".format(digest)
def get_profile(user):
"""
Return profile instance for user. Create profile if it does not exist.
"""
try:
return user.profile
except Profile.DoesNotExist:
return Profile.objects.create(user=user)
User.get_profile = get_profile