refactor: extract code share between question sets

This commit is contained in:
James Graham
2020-12-07 14:19:00 +00:00
parent 6bb4f09454
commit e045b084d0
3 changed files with 128 additions and 171 deletions

View File

@@ -1,18 +1,18 @@
import logging import logging
import typing
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField from django_countries.fields import CountryField
from django_settings_export import settings_export from django_settings_export import settings_export
from post_office import mail from post_office import mail
from .question import AnswerSet, Question, QuestionChoice
logger = logging.getLogger(__name__) # pylint: disable=invalid-name logger = logging.getLogger(__name__) # pylint: disable=invalid-name
__all__ = [ __all__ = [
@@ -83,77 +83,18 @@ class Theme(models.Model):
return self.name return self.name
class PersonQuestion(models.Model): class PersonQuestion(Question):
"""Question which may be asked about a person.""" """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): class PersonQuestionChoice(QuestionChoice):
""" """Allowed answer to a :class:`PersonQuestion`."""
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 to which this answer belongs
question = models.ForeignKey(PersonQuestion, related_name='answers', question = models.ForeignKey(PersonQuestion,
related_name='answers',
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, null=False) 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): class Person(models.Model):
@@ -197,13 +138,8 @@ class Person(models.Model):
return self.name return self.name
class PersonAnswerSet(models.Model): class PersonAnswerSet(AnswerSet):
"""The answers to the person questions at a particular point in time.""" """The answers to the person questions at a particular point in time."""
class Meta:
ordering = [
'timestamp',
]
#: Person to which this answer set belongs #: Person to which this answer set belongs
person = models.ForeignKey(Person, person = models.ForeignKey(Person,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -214,13 +150,6 @@ class PersonAnswerSet(models.Model):
#: Answers to :class:`PersonQuestion`s #: Answers to :class:`PersonQuestion`s
question_answers = models.ManyToManyField(PersonQuestionChoice) 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)
################## ##################
# Static questions # Static questions

103
people/models/question.py Normal file
View File

@@ -0,0 +1,103 @@
"""Base models for configurable questions and response sets."""
import typing
from django.db import models
from django.utils.text import slugify
class Question(models.Model):
"""Questions from which a survey form can be created."""
class Meta:
abstract = True
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:`QuestionChoice`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 QuestionChoice(models.Model):
"""Allowed answer to a :class:`Question`."""
class Meta:
abstract = True
constraints = [
models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer')
]
ordering = [
'question__order',
'order',
'text',
]
#: Question to which this answer belongs
#: This foreign key must be added to each concrete subclass
# question = models.ForeignKey(Question,
# 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 AnswerSet(models.Model):
"""The answers to a set of questions at a particular point in time."""
class Meta:
abstract = True
ordering = [
'timestamp',
]
#: Entity to which this answer set belongs
#: This foreign key must be added to each concrete subclass
# person = models.ForeignKey(Person,
# on_delete=models.CASCADE,
# related_name='answer_sets',
# blank=False,
# null=False)
#: Answers to :class:`Question`s
#: This many to many relation must be added to each concrete subclass
# question_answers = models.ManyToManyField(QuestionChoice)
#: When were these answers collected?
timestamp = models.DateTimeField(auto_now_add=True, editable=False)
#: When were these answers replaced? - happens when another set is collected
replaced_timestamp = models.DateTimeField(blank=True,
null=True,
editable=False)

View File

@@ -2,14 +2,12 @@
Models describing relationships between people. Models describing relationships between people.
""" """
import typing
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify
from .person import Person from .person import Person
from .question import AnswerSet, Question, QuestionChoice
__all__ = [ __all__ = [
'RelationshipQuestion', 'RelationshipQuestion',
@@ -19,79 +17,19 @@ __all__ = [
] ]
class RelationshipQuestion(models.Model): class RelationshipQuestion(Question):
""" """Question which may be asked about a relationship."""
Question which may be asked about a relationship.
"""
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:`RelationshipQuestionChoice`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 RelationshipQuestionChoice(models.Model): class RelationshipQuestionChoice(QuestionChoice):
""" """Allowed answer to a :class:`RelationshipQuestion`."""
Allowed answer to a :class:`RelationshipQuestion`.
"""
class Meta:
constraints = [
models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer')
]
ordering = [
'question__order',
'order',
'text',
]
#: Question to which this answer belongs #: Question to which this answer belongs
question = models.ForeignKey(RelationshipQuestion, related_name='answers', question = models.ForeignKey(RelationshipQuestion,
related_name='answers',
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, null=False) 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 ExternalPerson(models.Model): # class ExternalPerson(models.Model):
@@ -173,31 +111,18 @@ class Relationship(models.Model):
target_person=self.source) target_person=self.source)
class RelationshipAnswerSet(models.Model): class RelationshipAnswerSet(AnswerSet):
""" """The answers to the relationship questions at a particular point in time."""
The answers to the relationship questions at a particular point in time.
"""
class Meta:
ordering = [
'timestamp',
]
#: Relationship to which this answer set belongs #: Relationship to which this answer set belongs
relationship = models.ForeignKey(Relationship, relationship = models.ForeignKey(Relationship,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='answer_sets', related_name='answer_sets',
blank=False, null=False) blank=False,
null=False)
#: Answers to :class:`RelationshipQuestion`s #: Answers to :class:`RelationshipQuestion`s
question_answers = models.ManyToManyField(RelationshipQuestionChoice) question_answers = models.ManyToManyField(RelationshipQuestionChoice)
#: 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): def get_absolute_url(self):
return self.relationship.get_absolute_url() return self.relationship.get_absolute_url()