feat: add organisation questions and admin pages

See #76
This commit is contained in:
James Graham
2021-02-24 12:08:52 +00:00
parent 25cc33e2c1
commit adf12442a4
3 changed files with 173 additions and 1 deletions

View File

@@ -16,9 +16,29 @@ class CustomUserAdmin(UserAdmin):
) # yapf: disable ) # yapf: disable
class OrganisationQuestionChoiceInline(admin.TabularInline):
model = models.OrganisationQuestionChoice
@admin.register(models.OrganisationQuestion)
class OrganisationQuestionAdmin(admin.ModelAdmin):
inlines = [
OrganisationQuestionChoiceInline,
]
class OrganisationAnswerSetInline(admin.TabularInline):
model = models.OrganisationAnswerSet
readonly_fields = [
'question_answers',
]
@admin.register(models.Organisation) @admin.register(models.Organisation)
class OrganisationAdmin(admin.ModelAdmin): class OrganisationAdmin(admin.ModelAdmin):
pass inlines = [
OrganisationAnswerSetInline,
]
@admin.register(models.Theme) @admin.register(models.Theme)

View File

@@ -0,0 +1,61 @@
# Generated by Django 2.2.10 on 2021-02-23 13:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('people', '0034_remove_personanswerset_disciplines'),
]
operations = [
migrations.CreateModel(
name='OrganisationQuestion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.PositiveSmallIntegerField(default=1)),
('text', models.CharField(max_length=255)),
('is_multiple_choice', models.BooleanField(default=False)),
('allow_free_text', models.BooleanField(default=False)),
('order', models.SmallIntegerField(default=0)),
('answer_is_public', models.BooleanField(default=True, help_text='Should answers to this question be displayed on profiles?')),
],
options={
'ordering': ['order', 'text'],
'abstract': False,
},
),
migrations.CreateModel(
name='OrganisationQuestionChoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.CharField(max_length=255)),
('order', models.SmallIntegerField(default=0)),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='people.OrganisationQuestion')),
],
options={
'ordering': ['question__order', 'order', 'text'],
'abstract': False,
},
),
migrations.CreateModel(
name='OrganisationAnswerSet',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now_add=True)),
('replaced_timestamp', models.DateTimeField(blank=True, editable=False, null=True)),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answer_sets', to='people.Organisation')),
('question_answers', models.ManyToManyField(to='people.OrganisationQuestionChoice')),
],
options={
'ordering': ['timestamp'],
'abstract': False,
},
),
migrations.AddConstraint(
model_name='organisationquestionchoice',
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer'),
),
]

View File

@@ -17,7 +17,10 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name
__all__ = [ __all__ = [
'User', 'User',
'OrganisationQuestion',
'OrganisationQuestionChoice',
'Organisation', 'Organisation',
'OrganisationAnswerSet',
'Theme', 'Theme',
'PersonQuestion', 'PersonQuestion',
'PersonQuestionChoice', 'PersonQuestionChoice',
@@ -66,6 +69,26 @@ class User(AbstractUser):
self.username) self.username)
class OrganisationQuestion(Question):
"""Question which may be asked about a Organisation."""
#: Should answers to this question be displayed on public profiles?
answer_is_public = models.BooleanField(
help_text='Should answers to this question be displayed on profiles?',
default=True,
blank=False,
null=False)
class OrganisationQuestionChoice(QuestionChoice):
"""Allowed answer to a :class:`OrganisationQuestion`."""
#: Question to which this answer belongs
question = models.ForeignKey(OrganisationQuestion,
related_name='answers',
on_delete=models.CASCADE,
blank=False,
null=False)
class Organisation(models.Model): class Organisation(models.Model):
"""Organisation to which a :class:`Person` belongs.""" """Organisation to which a :class:`Person` belongs."""
name = models.CharField(max_length=255, blank=False, null=False) name = models.CharField(max_length=255, blank=False, null=False)
@@ -79,10 +102,78 @@ class Organisation(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@property
def current_answers(self) -> 'OrganisationAnswerSet':
return self.answer_sets.last()
def get_absolute_url(self): def get_absolute_url(self):
return reverse('people:organisation.detail', kwargs={'pk': self.pk}) return reverse('people:organisation.detail', kwargs={'pk': self.pk})
class OrganisationAnswerSet(AnswerSet):
"""The answers to the organisation questions at a particular point in time."""
#: Organisation to which this answer set belongs
organisation = models.ForeignKey(Organisation,
on_delete=models.CASCADE,
related_name='answer_sets',
blank=False,
null=False)
#: Answers to :class:`OrganisationQuestion`s
question_answers = models.ManyToManyField(OrganisationQuestionChoice)
def public_answers(self) -> models.QuerySet:
"""Get answers to questions which are public."""
return self.question_answers.filter(question__answer_is_public=True)
def as_dict(self):
"""Get the answers from this set as a dictionary for use in Form.initial."""
exclude_fields = {
'id',
'timestemp',
'replaced_timestamp',
'organisation_id',
'question_answers',
}
def field_value_repr(field):
"""Get the representation of a field's value as required by Form.initial."""
attr_val = getattr(self, field.attname)
# Relation fields need to return PKs
if isinstance(field, models.ManyToManyField):
return [obj.pk for obj in attr_val.all()]
# But foreign key fields are a PK already so no extra work
return attr_val
answers = {
# Foreign key fields have _id at end in model _meta but don't in forms
field.attname.rstrip('_id'): field_value_repr(field)
for field in self._meta.get_fields()
if field.attname not in exclude_fields
}
for answer in self.question_answers.all():
question = answer.question
field_name = f'question_{question.pk}'
if question.is_multiple_choice:
if field_name not in answers:
answers[field_name] = []
answers[field_name].append(answer.pk)
else:
answers[field_name] = answer.pk
return answers
def get_absolute_url(self):
return self.organisation.get_absolute_url()
class Theme(models.Model): class Theme(models.Model):
""" """
Project theme within which a :class:`Person` works. Project theme within which a :class:`Person` works.