Files
breccia-mapper/people/models/person.py
2020-11-26 14:10:38 +00:00

257 lines
7.7 KiB
Python

import logging
import typing
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
from django_settings_export import settings_export
from post_office import mail
from backports.db.models.enums import TextChoices
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
__all__ = [
'User',
'Organisation',
'Theme',
'PersonQuestion',
'PersonQuestionChoice',
'Person',
'PersonAnswerSet',
]
class User(AbstractUser):
"""
Custom user model in case we need to make changes later.
"""
email = models.EmailField(_('email address'), blank=False, null=False)
def has_person(self) -> bool:
"""
Does this user have a linked :class:`Person` record?
"""
return hasattr(self, 'person')
def send_welcome_email(self) -> None:
"""Send a welcome email to a new user."""
# Get exported data from settings.py first
context = settings_export(None)
context.update({
'user': self,
})
logger.info('Sending welcome mail to user \'%s\'', self.username)
try:
mail.send(
[self.email],
sender=settings.DEFAULT_FROM_EMAIL,
template=settings.TEMPLATE_WELCOME_EMAIL_NAME,
context=context,
priority='now' # Send immediately - don't add to queue
)
except ValidationError:
logger.error(
'Sending welcome mail failed, invalid email for user \'%s\'',
self.username)
class Organisation(models.Model):
"""
Organisation to which a :class:`Person` belongs.
"""
name = models.CharField(max_length=255, blank=False, null=False)
def __str__(self) -> str:
return self.name
class Theme(models.Model):
"""
Project theme within which a :class:`Person` works.
"""
name = models.CharField(max_length=255, blank=False, null=False)
def __str__(self) -> str:
return self.name
class PersonQuestion(models.Model):
"""Question which may be asked about a person."""
class Meta:
ordering = [
'order',
'text',
]
#: Version number of this question - to allow modification without invalidating existing data
version = models.PositiveSmallIntegerField(default=1,
blank=False, null=False)
#: Text of question
text = models.CharField(max_length=255,
blank=False, null=False)
#: Position of this question in the list
order = models.SmallIntegerField(default=0,
blank=False, null=False)
@property
def choices(self) -> typing.List[typing.List[str]]:
"""
Convert the :class:`PersonQuestionChoice`s for this question into Django choices.
"""
return [
[choice.pk, str(choice)] for choice in self.answers.all()
]
@property
def slug(self) -> str:
return slugify(self.text)
def __str__(self) -> str:
return self.text
class PersonQuestionChoice(models.Model):
"""
Allowed answer to a :class:`PersonQuestion`.
"""
class Meta:
constraints = [
models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer')
]
ordering = [
'question__order',
'order',
'text',
]
#: Question to which this answer belongs
question = models.ForeignKey(PersonQuestion, related_name='answers',
on_delete=models.CASCADE,
blank=False, null=False)
#: Text of answer
text = models.CharField(max_length=255,
blank=False, null=False)
#: Position of this answer in the list
order = models.SmallIntegerField(default=0,
blank=False, null=False)
@property
def slug(self) -> str:
return slugify(self.text)
def __str__(self) -> str:
return self.text
class Person(models.Model):
"""
A person may be a member of the BRECcIA core team or an external stakeholder.
"""
class Meta:
verbose_name_plural = 'people'
#: User account belonging to this person
user = models.OneToOneField(settings.AUTH_USER_MODEL,
related_name='person',
on_delete=models.CASCADE,
blank=True,
null=True)
#: Name of the person
name = models.CharField(max_length=255, blank=False, null=False)
#: People with whom this person has relationship - via intermediate :class:`Relationship` model
relationship_targets = models.ManyToManyField(
'self',
related_name='relationship_sources',
through='Relationship',
through_fields=('source', 'target'),
symmetrical=False)
###############################################################
# Data collected for analysis of community makeup and structure
nationality = CountryField(blank=True, null=True)
country_of_residence = CountryField(blank=True, null=True)
#: Organisation this person is employed by or affiliated with
organisation = models.ForeignKey(Organisation,
on_delete=models.PROTECT,
related_name='members',
blank=True,
null=True)
#: When did this person start at their current organisation?
organisation_started_date = models.DateField(
'Date started at this organisation', blank=False, null=True)
#: Job title this person holds within their organisation
job_title = models.CharField(max_length=255, blank=True, null=False)
#: Discipline(s) within which this person works
disciplines = models.CharField(max_length=255, blank=True, null=True)
#: Project themes within this person works
themes = models.ManyToManyField(Theme, related_name='people', blank=True)
@property
def relationships(self):
return self.relationships_as_source.all().union(
self.relationships_as_target.all())
@property
def current_answers(self) -> 'PersonAnswerSet':
return self.answer_sets.last()
def get_absolute_url(self):
return reverse('people:person.detail', kwargs={'pk': self.pk})
def __str__(self) -> str:
return self.name
class PersonAnswerSet(models.Model):
"""
The answers to the person questions at a particular point in time.
"""
class Meta:
ordering = [
'timestamp',
]
#: Person to which this answer set belongs
person = models.ForeignKey(Person,
on_delete=models.CASCADE,
related_name='answer_sets',
blank=False, null=False)
#: Answers to :class:`PersonQuestion`s
question_answers = models.ManyToManyField(PersonQuestionChoice)
#: When were these answers collected?
timestamp = models.DateTimeField(auto_now_add=True,
editable=False)
replaced_timestamp = models.DateTimeField(blank=True, null=True,
editable=False)
def get_absolute_url(self):
return self.person.get_absolute_url()