mirror of
https://github.com/Southampton-RSG/breccia-mapper.git
synced 2026-03-03 03:17:07 +00:00
Merge remote-tracking branch 'origin/dev'
This commit is contained in:
@@ -19,7 +19,11 @@ from django.urls import include, path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('admin/',
|
||||
admin.site.urls),
|
||||
|
||||
path('select2/',
|
||||
include('django_select2.urls')),
|
||||
|
||||
path('',
|
||||
include('django.contrib.auth.urls')),
|
||||
@@ -36,4 +40,4 @@ urlpatterns = [
|
||||
|
||||
path('',
|
||||
include('activities.urls')),
|
||||
]
|
||||
] # yapf: disable
|
||||
|
||||
@@ -2,3 +2,9 @@ from . import (
|
||||
activities,
|
||||
people
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'activities',
|
||||
'people',
|
||||
]
|
||||
|
||||
@@ -4,3 +4,10 @@ from . import (
|
||||
activities,
|
||||
people
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'activities',
|
||||
'people',
|
||||
'ExportListView',
|
||||
]
|
||||
|
||||
@@ -21,19 +21,34 @@ class OrganisationAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(models.Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(models.Theme)
|
||||
class ThemeAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class PersonQuestionChoiceInline(admin.TabularInline):
|
||||
model = models.PersonQuestionChoice
|
||||
|
||||
|
||||
@admin.register(models.PersonQuestion)
|
||||
class PersonQuestionAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
PersonQuestionChoiceInline,
|
||||
]
|
||||
|
||||
|
||||
class PersonAnswerSetInline(admin.TabularInline):
|
||||
model = models.PersonAnswerSet
|
||||
readonly_fields = [
|
||||
'question_answers',
|
||||
]
|
||||
|
||||
|
||||
@admin.register(models.Person)
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
inlines = [
|
||||
PersonAnswerSetInline,
|
||||
]
|
||||
|
||||
|
||||
class RelationshipQuestionChoiceInline(admin.TabularInline):
|
||||
|
||||
@@ -8,7 +8,7 @@ from django import forms
|
||||
from django.forms.widgets import SelectDateWidget
|
||||
from django.utils import timezone
|
||||
|
||||
from django_select2.forms import Select2Widget, Select2MultipleWidget
|
||||
from django_select2.forms import ModelSelect2Widget, Select2Widget, Select2MultipleWidget
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -25,22 +25,58 @@ def get_date_year_range() -> typing.Iterable[int]:
|
||||
|
||||
|
||||
class PersonForm(forms.ModelForm):
|
||||
"""
|
||||
Form for creating / updating an instance of :class:`Person`.
|
||||
"""
|
||||
"""Form for creating / updating an instance of :class:`Person`."""
|
||||
class Meta:
|
||||
model = models.Person
|
||||
fields = [
|
||||
'name',
|
||||
'gender',
|
||||
'age_group',
|
||||
]
|
||||
|
||||
|
||||
class RelationshipForm(forms.Form):
|
||||
target = forms.ModelChoiceField(
|
||||
models.Person.objects.all(),
|
||||
widget=ModelSelect2Widget(search_fields=['name__icontains']))
|
||||
|
||||
|
||||
class DynamicAnswerSetBase(forms.Form):
|
||||
field_class = forms.ModelChoiceField
|
||||
field_widget = None
|
||||
field_required = True
|
||||
question_model = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for question in self.question_model.objects.all():
|
||||
field_class = self.field_class
|
||||
field_widget = self.field_widget
|
||||
|
||||
if question.is_multiple_choice:
|
||||
field_class = forms.ModelMultipleChoiceField
|
||||
field_widget = Select2MultipleWidget
|
||||
|
||||
field = field_class(label=question,
|
||||
queryset=question.answers,
|
||||
widget=field_widget,
|
||||
required=self.field_required)
|
||||
self.fields['question_{}'.format(question.pk)] = field
|
||||
|
||||
|
||||
class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
"""Form for variable person attributes.
|
||||
|
||||
Dynamic fields inspired by https://jacobian.org/2010/feb/28/dynamic-form-generation/
|
||||
"""
|
||||
class Meta:
|
||||
model = models.PersonAnswerSet
|
||||
fields = [
|
||||
'nationality',
|
||||
'country_of_residence',
|
||||
'organisation',
|
||||
'organisation_started_date',
|
||||
'job_title',
|
||||
'disciplines',
|
||||
'role',
|
||||
'themes',
|
||||
]
|
||||
widgets = {
|
||||
@@ -53,27 +89,24 @@ class PersonForm(forms.ModelForm):
|
||||
'If you don\'t know the exact date, an approximate date is okay.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
question_model = models.PersonQuestion
|
||||
|
||||
self.fields['organisation_started_date'].widget = SelectDateWidget(
|
||||
years=get_date_year_range())
|
||||
def save(self, commit=True) -> models.PersonAnswerSet:
|
||||
# Save Relationship model
|
||||
self.instance = super().save(commit=commit)
|
||||
|
||||
if commit:
|
||||
# Save answers to relationship questions
|
||||
for key, value in self.cleaned_data.items():
|
||||
if key.startswith('question_') and value:
|
||||
try:
|
||||
self.instance.question_answers.add(value)
|
||||
|
||||
class DynamicAnswerSetBase(forms.Form):
|
||||
field_class = forms.ModelChoiceField
|
||||
field_widget = None
|
||||
field_required = True
|
||||
except TypeError:
|
||||
# Value is a QuerySet - multiple choice question
|
||||
self.instance.question_answers.add(*value.all())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for question in models.RelationshipQuestion.objects.all():
|
||||
field = self.field_class(label=question,
|
||||
queryset=question.answers,
|
||||
widget=self.field_widget,
|
||||
required=self.field_required)
|
||||
self.fields['question_{}'.format(question.pk)] = field
|
||||
return self.instance
|
||||
|
||||
|
||||
class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
@@ -88,6 +121,8 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
'relationship',
|
||||
]
|
||||
|
||||
question_model = models.RelationshipQuestion
|
||||
|
||||
def save(self, commit=True) -> models.RelationshipAnswerSet:
|
||||
# Save Relationship model
|
||||
self.instance = super().save(commit=commit)
|
||||
@@ -96,7 +131,12 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
# Save answers to relationship questions
|
||||
for key, value in self.cleaned_data.items():
|
||||
if key.startswith('question_') and value:
|
||||
self.instance.question_answers.add(value)
|
||||
try:
|
||||
self.instance.question_answers.add(value)
|
||||
|
||||
except TypeError:
|
||||
# Value is a QuerySet - multiple choice question
|
||||
self.instance.question_answers.add(*value.all())
|
||||
|
||||
return self.instance
|
||||
|
||||
@@ -108,6 +148,7 @@ class NetworkFilterForm(DynamicAnswerSetBase):
|
||||
field_class = forms.ModelMultipleChoiceField
|
||||
field_widget = Select2MultipleWidget
|
||||
field_required = False
|
||||
question_model = models.RelationshipQuestion
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
55
people/migrations/0022_refactor_person_questions.py
Normal file
55
people/migrations/0022_refactor_person_questions.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Generated by Django 2.2.10 on 2020-11-23 14:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('people', '0021_refactor_person_disciplines'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PersonQuestion',
|
||||
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)),
|
||||
('order', models.SmallIntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['order', 'text'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PersonQuestionChoice',
|
||||
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.PersonQuestion')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['question__order', 'order', 'text'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PersonAnswerSet',
|
||||
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)),
|
||||
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answer_sets', to='people.Person')),
|
||||
('question_answers', models.ManyToManyField(to='people.PersonQuestionChoice')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['timestamp'],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='personquestionchoice',
|
||||
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer'),
|
||||
),
|
||||
]
|
||||
65
people/migrations/0023_remove_person_role.py
Normal file
65
people/migrations/0023_remove_person_role.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Generated by Django 2.2.10 on 2020-11-25 15:50
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import migrations
|
||||
from django.utils import timezone
|
||||
|
||||
from .utils.question_sets import port_question
|
||||
|
||||
|
||||
def migrate_forward(apps, schema_editor):
|
||||
Person = apps.get_model('people', 'Person')
|
||||
Role = apps.get_model('people', 'Role')
|
||||
|
||||
role_question = port_question(apps, 'Role',
|
||||
Role.objects.values_list('name', flat=True))
|
||||
|
||||
for person in Person.objects.all():
|
||||
try:
|
||||
prev_set = person.answer_sets.latest('timestamp')
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
prev_set = None
|
||||
|
||||
try:
|
||||
|
||||
answer_set = person.answer_sets.create()
|
||||
answer_set.question_answers.add(
|
||||
role_question.answers.get(text=person.role.name))
|
||||
|
||||
prev_set.replaced_timestamp = timezone.datetime.now()
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
def migrate_backward(apps, schema_editor):
|
||||
Person = apps.get_model('people', 'Person')
|
||||
Role = apps.get_model('people', 'Role')
|
||||
|
||||
for person in Person.objects.all():
|
||||
try:
|
||||
current_answers = person.answer_sets.latest('timestamp')
|
||||
role_answer = current_answers.question_answers.get(
|
||||
question__text='Role')
|
||||
person.role, _ = Role.objects.get_or_create(name=role_answer.text)
|
||||
person.save()
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('people', '0022_refactor_person_questions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_forward, migrate_backward),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='role',
|
||||
),
|
||||
migrations.DeleteModel('Role'),
|
||||
]
|
||||
120
people/migrations/0024_remove_age_gender.py
Normal file
120
people/migrations/0024_remove_age_gender.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Generated by Django 2.2.10 on 2020-11-26 13:03
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import migrations
|
||||
|
||||
from backports.db.models.enums import TextChoices
|
||||
from .utils.question_sets import port_question
|
||||
|
||||
|
||||
class GenderChoices(TextChoices):
|
||||
MALE = 'M', 'Male'
|
||||
FEMALE = 'F', 'Female'
|
||||
OTHER = 'O', 'Other'
|
||||
PREFER_NOT_TO_SAY = 'N', 'Prefer not to say'
|
||||
|
||||
|
||||
class AgeGroupChoices(TextChoices):
|
||||
LTE_25 = '<=25', '25 or under'
|
||||
BETWEEN_26_30 = '26-30', '26-30'
|
||||
BETWEEN_31_35 = '31-35', '31-35'
|
||||
BETWEEN_36_40 = '36-40', '36-40'
|
||||
BETWEEN_41_45 = '41-45', '41-45'
|
||||
BETWEEN_46_50 = '46-50', '46-50'
|
||||
BETWEEN_51_55 = '51-55', '51-55'
|
||||
BETWEEN_56_60 = '56-60', '56-60'
|
||||
GTE_61 = '>=61', '61 or older'
|
||||
PREFER_NOT_TO_SAY = 'N', 'Prefer not to say'
|
||||
|
||||
|
||||
def migrate_forward(apps, schema_editor):
|
||||
Person = apps.get_model('people', 'Person')
|
||||
|
||||
gender_question = port_question(apps, 'Gender', GenderChoices.labels)
|
||||
age_question = port_question(apps, 'Age', AgeGroupChoices.labels)
|
||||
|
||||
for person in Person.objects.all():
|
||||
try:
|
||||
answer_set = person.answer_sets.latest('timestamp')
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
answer_set = person.answer_sets.create()
|
||||
|
||||
try:
|
||||
gender = [
|
||||
item for item in GenderChoices if item.value == person.gender
|
||||
][0]
|
||||
answer_set.question_answers.filter(
|
||||
question__text=gender_question.text).delete()
|
||||
answer_set.question_answers.add(
|
||||
gender_question.answers.get(text__iexact=gender.label))
|
||||
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
try:
|
||||
age = [
|
||||
item for item in AgeGroupChoices
|
||||
if item.value == person.age_group
|
||||
][0]
|
||||
answer_set.question_answers.filter(
|
||||
question__text=age_question.text).delete()
|
||||
answer_set.question_answers.add(
|
||||
age_question.answers.get(text__iexact=age.label))
|
||||
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
|
||||
def migrate_backward(apps, schema_editor):
|
||||
Person = apps.get_model('people', 'Person')
|
||||
|
||||
for person in Person.objects.all():
|
||||
try:
|
||||
current_answers = person.answer_sets.latest('timestamp')
|
||||
age_answer = current_answers.question_answers.get(
|
||||
question__text='Age')
|
||||
|
||||
person.age_group = [
|
||||
item for item in AgeGroupChoices
|
||||
if item.label == age_answer.text
|
||||
][0].value
|
||||
|
||||
person.save()
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
current_answers = person.answer_sets.latest('timestamp')
|
||||
gender_answer = current_answers.question_answers.get(
|
||||
question__text='Gender')
|
||||
person.gender = [
|
||||
|
||||
item for item in GenderChoices
|
||||
if item.label == gender_answer.text
|
||||
][0].value
|
||||
|
||||
person.save()
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('people', '0023_remove_person_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_forward, migrate_backward),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='age_group',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='gender',
|
||||
),
|
||||
]
|
||||
26
people/migrations/0025_rename_relationship_target.py
Normal file
26
people/migrations/0025_rename_relationship_target.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 2.2.10 on 2020-11-27 08:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('people', '0024_remove_age_gender'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='relationship',
|
||||
name='unique_relationship',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='relationship',
|
||||
old_name='target',
|
||||
new_name='target_person',
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='relationship',
|
||||
constraint=models.UniqueConstraint(fields=('source', 'target_person'), name='unique_relationship'),
|
||||
),
|
||||
]
|
||||
148
people/migrations/0026_move_static_person_questions.py
Normal file
148
people/migrations/0026_move_static_person_questions.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# Generated by Django 2.2.10 on 2020-12-02 13:31
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_countries.fields
|
||||
|
||||
|
||||
def migrate_forward(apps, schema_editor):
|
||||
Person = apps.get_model('people', 'Person')
|
||||
PersonAnswerset = apps.get_model('people', 'PersonAnswerSet')
|
||||
|
||||
fields = {
|
||||
'country_of_residence',
|
||||
'disciplines',
|
||||
'job_title',
|
||||
'nationality',
|
||||
'organisation',
|
||||
'organisation_started_date',
|
||||
'themes',
|
||||
}
|
||||
|
||||
for person in Person.objects.all():
|
||||
try:
|
||||
answer_set = person.answer_sets.last()
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
answer_set = person.answer_sets.create()
|
||||
|
||||
for field in fields:
|
||||
value = getattr(person, field)
|
||||
try:
|
||||
setattr(answer_set, field, value)
|
||||
|
||||
except TypeError:
|
||||
# Cannot directly set an m2m field
|
||||
m2m = getattr(answer_set, field)
|
||||
m2m.set(value.all())
|
||||
|
||||
answer_set.save()
|
||||
|
||||
|
||||
def migrate_backward(apps, schema_editor):
|
||||
Person = apps.get_model('people', 'Person')
|
||||
PersonAnswerset = apps.get_model('people', 'PersonAnswerSet')
|
||||
|
||||
fields = {
|
||||
'country_of_residence',
|
||||
'disciplines',
|
||||
'job_title',
|
||||
'nationality',
|
||||
'organisation',
|
||||
'organisation_started_date',
|
||||
'themes',
|
||||
}
|
||||
|
||||
for person in Person.objects.all():
|
||||
try:
|
||||
answer_set = person.answer_sets.last()
|
||||
|
||||
for field in fields:
|
||||
value = getattr(answer_set, field)
|
||||
try:
|
||||
setattr(person, field, value)
|
||||
|
||||
except TypeError:
|
||||
# Cannot directly set an m2m field
|
||||
m2m = getattr(person, field)
|
||||
m2m.set(value.all())
|
||||
|
||||
person.save()
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('people', '0025_rename_relationship_target'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='personanswerset',
|
||||
name='country_of_residence',
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=2, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='personanswerset',
|
||||
name='disciplines',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='personanswerset',
|
||||
name='job_title',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='personanswerset',
|
||||
name='nationality',
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=2, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='personanswerset',
|
||||
name='organisation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='members', to='people.Organisation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='personanswerset',
|
||||
name='organisation_started_date',
|
||||
field=models.DateField(null=True, verbose_name='Date started at this organisation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='personanswerset',
|
||||
name='themes',
|
||||
field=models.ManyToManyField(blank=True, related_name='people', to='people.Theme'),
|
||||
),
|
||||
migrations.RunPython(migrate_forward, migrate_backward),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='country_of_residence',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='disciplines',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='job_title',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='nationality',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='organisation',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='organisation_started_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='person',
|
||||
name='themes',
|
||||
),
|
||||
]
|
||||
23
people/migrations/0027_multiple_choice_questions.py
Normal file
23
people/migrations/0027_multiple_choice_questions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.10 on 2020-12-07 16:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('people', '0026_move_static_person_questions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='personquestion',
|
||||
name='is_multiple_choice',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='relationshipquestion',
|
||||
name='is_multiple_choice',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
0
people/migrations/utils/__init__.py
Normal file
0
people/migrations/utils/__init__.py
Normal file
23
people/migrations/utils/question_sets.py
Normal file
23
people/migrations/utils/question_sets.py
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
import typing
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
||||
def port_question(apps, question_text: str,
|
||||
answers_text: typing.Iterable[str]):
|
||||
PersonQuestion = apps.get_model('people', 'PersonQuestion')
|
||||
|
||||
try:
|
||||
prev_question = PersonQuestion.objects.filter(
|
||||
text=question_text).latest('version')
|
||||
question = PersonQuestion.objects.create(
|
||||
text=question_text, version=prev_question.version + 1)
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
question = PersonQuestion.objects.create(text=question_text)
|
||||
|
||||
for answer_text in answers_text:
|
||||
question.answers.get_or_create(text=answer_text)
|
||||
|
||||
return question
|
||||
@@ -11,16 +11,18 @@ 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
|
||||
from .question import AnswerSet, Question, QuestionChoice
|
||||
|
||||
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
__all__ = [
|
||||
'User',
|
||||
'Organisation',
|
||||
'Role',
|
||||
'Theme',
|
||||
'PersonQuestion',
|
||||
'PersonQuestionChoice',
|
||||
'Person',
|
||||
'PersonAnswerSet',
|
||||
]
|
||||
|
||||
|
||||
@@ -36,7 +38,7 @@ class User(AbstractUser):
|
||||
"""
|
||||
return hasattr(self, 'person')
|
||||
|
||||
def send_welcome_email(self):
|
||||
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)
|
||||
@@ -71,16 +73,6 @@ class Organisation(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
"""
|
||||
Role which a :class:`Person` holds within the project.
|
||||
"""
|
||||
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.
|
||||
@@ -91,6 +83,20 @@ class Theme(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class PersonQuestion(Question):
|
||||
"""Question which may be asked about a person."""
|
||||
|
||||
|
||||
class PersonQuestionChoice(QuestionChoice):
|
||||
"""Allowed answer to a :class:`PersonQuestion`."""
|
||||
#: Question to which this answer belongs
|
||||
question = models.ForeignKey(PersonQuestion,
|
||||
related_name='answers',
|
||||
on_delete=models.CASCADE,
|
||||
blank=False,
|
||||
null=False)
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
"""
|
||||
A person may be a member of the BRECcIA core team or an external stakeholder.
|
||||
@@ -113,39 +119,39 @@ class Person(models.Model):
|
||||
'self',
|
||||
related_name='relationship_sources',
|
||||
through='Relationship',
|
||||
through_fields=('source', 'target'),
|
||||
through_fields=('source', 'target_person'),
|
||||
symmetrical=False)
|
||||
|
||||
###############################################################
|
||||
# Data collected for analysis of community makeup and structure
|
||||
@property
|
||||
def relationships(self):
|
||||
return self.relationships_as_source.all().union(
|
||||
self.relationships_as_target.all())
|
||||
|
||||
class GenderChoices(TextChoices):
|
||||
MALE = 'M', _('Male')
|
||||
FEMALE = 'F', _('Female')
|
||||
OTHER = 'O', _('Other')
|
||||
PREFER_NOT_TO_SAY = 'N', _('Prefer not to say')
|
||||
@property
|
||||
def current_answers(self) -> 'PersonAnswerSet':
|
||||
return self.answer_sets.last()
|
||||
|
||||
gender = models.CharField(max_length=1,
|
||||
choices=GenderChoices.choices,
|
||||
blank=True,
|
||||
null=False)
|
||||
def get_absolute_url(self):
|
||||
return reverse('people:person.detail', kwargs={'pk': self.pk})
|
||||
|
||||
class AgeGroupChoices(TextChoices):
|
||||
LTE_25 = '<=25', _('25 or under')
|
||||
BETWEEN_26_30 = '26-30', _('26-30')
|
||||
BETWEEN_31_35 = '31-35', _('31-35')
|
||||
BETWEEN_36_40 = '36-40', _('36-40')
|
||||
BETWEEN_41_45 = '41-45', _('41-45')
|
||||
BETWEEN_46_50 = '46-50', _('46-50')
|
||||
BETWEEN_51_55 = '51-55', _('51-55')
|
||||
BETWEEN_56_60 = '56-60', _('56-60')
|
||||
GTE_61 = '>=61', _('61 or older')
|
||||
PREFER_NOT_TO_SAY = 'N', _('Prefer not to say')
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
age_group = models.CharField(max_length=5,
|
||||
choices=AgeGroupChoices.choices,
|
||||
blank=True,
|
||||
null=False)
|
||||
|
||||
class PersonAnswerSet(AnswerSet):
|
||||
"""The answers to the person questions at a particular point in time."""
|
||||
#: 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)
|
||||
|
||||
##################
|
||||
# Static questions
|
||||
|
||||
nationality = CountryField(blank=True, null=True)
|
||||
|
||||
@@ -168,23 +174,8 @@ class Person(models.Model):
|
||||
#: Discipline(s) within which this person works
|
||||
disciplines = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
#: Role this person holds within the project
|
||||
role = models.ForeignKey(Role,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='holders',
|
||||
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())
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('people:person.detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
return self.person.get_absolute_url()
|
||||
|
||||
108
people/models/question.py
Normal file
108
people/models/question.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""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)
|
||||
|
||||
#: Should people be able to select multiple responses to this question?
|
||||
is_multiple_choice = models.BooleanField(default=False,
|
||||
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)
|
||||
@@ -2,13 +2,12 @@
|
||||
Models describing relationships between people.
|
||||
"""
|
||||
|
||||
import typing
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from .person import Person
|
||||
from .question import AnswerSet, Question, QuestionChoice
|
||||
|
||||
__all__ = [
|
||||
'RelationshipQuestion',
|
||||
@@ -18,79 +17,32 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class RelationshipQuestion(models.Model):
|
||||
"""
|
||||
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 RelationshipQuestion(Question):
|
||||
"""Question which may be asked about a relationship."""
|
||||
|
||||
|
||||
class RelationshipQuestionChoice(models.Model):
|
||||
"""
|
||||
Allowed answer to a :class:`RelationshipQuestion`.
|
||||
"""
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['question', 'text'],
|
||||
name='unique_question_answer')
|
||||
]
|
||||
ordering = [
|
||||
'question__order',
|
||||
'order',
|
||||
'text',
|
||||
]
|
||||
class RelationshipQuestionChoice(QuestionChoice):
|
||||
"""Allowed answer to a :class:`RelationshipQuestion`."""
|
||||
|
||||
#: Question to which this answer belongs
|
||||
question = models.ForeignKey(RelationshipQuestion, related_name='answers',
|
||||
question = models.ForeignKey(RelationshipQuestion,
|
||||
related_name='answers',
|
||||
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)
|
||||
# class ExternalPerson(models.Model):
|
||||
# """Model representing a person external to the project.
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
return slugify(self.text)
|
||||
# These will never need to be linked to a :class:`User` as they
|
||||
# will never log in to the system.
|
||||
# """
|
||||
# name = models.CharField(max_length=255,
|
||||
# blank=False, null=False)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
# def __str__(self) -> str:
|
||||
# return self.name
|
||||
|
||||
|
||||
class Relationship(models.Model):
|
||||
@@ -100,7 +52,7 @@ class Relationship(models.Model):
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['source', 'target'],
|
||||
models.UniqueConstraint(fields=['source', 'target_person'],
|
||||
name='unique_relationship'),
|
||||
]
|
||||
|
||||
@@ -110,16 +62,34 @@ class Relationship(models.Model):
|
||||
blank=False, null=False)
|
||||
|
||||
#: Person with whom the relationship is reported
|
||||
target = models.ForeignKey(Person, related_name='relationships_as_target',
|
||||
on_delete=models.CASCADE,
|
||||
blank=False, null=False)
|
||||
|
||||
target_person = models.ForeignKey(Person,
|
||||
related_name='relationships_as_target',
|
||||
on_delete=models.CASCADE,
|
||||
blank=False,
|
||||
null=False)
|
||||
# blank=True,
|
||||
# null=True)
|
||||
|
||||
# target_external_person = models.ForeignKey(
|
||||
# ExternalPerson,
|
||||
# related_name='relationships_as_target',
|
||||
# on_delete=models.CASCADE,
|
||||
# blank=True,
|
||||
# null=True)
|
||||
|
||||
#: When was this relationship defined?
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
#: When was this marked as expired? Default None means it has not expired
|
||||
expired = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
@property
|
||||
def target(self) -> Person:
|
||||
if self.target_person:
|
||||
return self.target_person
|
||||
|
||||
raise ObjectDoesNotExist('Relationship has no target linked')
|
||||
|
||||
@property
|
||||
def current_answers(self) -> 'RelationshipAnswerSet':
|
||||
return self.answer_sets.last()
|
||||
@@ -137,35 +107,22 @@ class Relationship(models.Model):
|
||||
|
||||
@raise Relationship.DoesNotExist: When the reverse relationship is not known
|
||||
"""
|
||||
return type(self).objects.get(source=self.target,
|
||||
target=self.source)
|
||||
return type(self).objects.get(source=self.target_person,
|
||||
target_person=self.source)
|
||||
|
||||
|
||||
class RelationshipAnswerSet(models.Model):
|
||||
"""
|
||||
The answers to the relationship questions at a particular point in time.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
ordering = [
|
||||
'timestamp',
|
||||
]
|
||||
class RelationshipAnswerSet(AnswerSet):
|
||||
"""The answers to the relationship questions at a particular point in time."""
|
||||
|
||||
#: Relationship to which this answer set belongs
|
||||
relationship = models.ForeignKey(Relationship,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='answer_sets',
|
||||
blank=False, null=False)
|
||||
blank=False,
|
||||
null=False)
|
||||
|
||||
#: Answers to :class:`RelationshipQuestion`s
|
||||
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):
|
||||
return self.relationship.get_absolute_url()
|
||||
|
||||
@@ -23,7 +23,9 @@ class UserIsLinkedPersonMixin(UserPassesTestMixin):
|
||||
test_person = self.get_object()
|
||||
|
||||
if not isinstance(test_person, models.Person):
|
||||
raise AttributeError('View incorrectly configured: \'related_person_field\' must be defined.')
|
||||
raise AttributeError(
|
||||
'View incorrectly configured: \'related_person_field\' must be defined.'
|
||||
)
|
||||
|
||||
return test_person
|
||||
|
||||
@@ -34,4 +36,5 @@ class UserIsLinkedPersonMixin(UserPassesTestMixin):
|
||||
Require that user is either staff or is the linked person.
|
||||
"""
|
||||
user = self.request.user
|
||||
return user.is_authenticated and (user.is_staff or self.get_test_person() == user.person)
|
||||
return user.is_authenticated and (
|
||||
user.is_staff or self.get_test_person() == user.person)
|
||||
|
||||
@@ -22,69 +22,81 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<dl>
|
||||
{% if person.gender %}
|
||||
<dt>Gender</dt>
|
||||
<dd>{{ person.get_gender_display }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if person.age_group %}
|
||||
<dt>Age Group</dt>
|
||||
<dd>{{ person.get_age_group_display }}</dd>
|
||||
{% endif %}
|
||||
{% with person.current_answers as answer_set %}
|
||||
<dl>
|
||||
</dl>
|
||||
|
||||
{% if person.nationality %}
|
||||
<dt>Nationality</dt>
|
||||
<dd>{{ person.nationality.name }}</dd>
|
||||
{% endif %}
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Question</th>
|
||||
<th>Answer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% if person.country_of_residence %}
|
||||
<dt>Country of Residence</dt>
|
||||
<dd>{{ person.country_of_residence.name }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if person.organisation %}
|
||||
<dt>Organisation</dt>
|
||||
<dd>{{ person.organisation }}</dd>
|
||||
|
||||
{% if person.organisation_started_date %}
|
||||
<dt>Started Date</dt>
|
||||
<dd>{{ person.organisation_started_date }}</dd>
|
||||
<tbody>
|
||||
{% if answer_set.nationality %}
|
||||
<tr><td>Nationality</td><td>{{ answer_set.nationality.name }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if person.job_title %}
|
||||
<dt>Job Title</dt>
|
||||
<dd>{{ person.job_title }}</dd>
|
||||
{% endif %}
|
||||
{% if answer_set.country_of_residence %}
|
||||
<tr><td>Country of Residence</td><td>{{ answer_set.country_of_residence.name }}</td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% if person.role %}
|
||||
<dt>Role</dt>
|
||||
<dd>{{ person.role }}</dd>
|
||||
{% endif %}
|
||||
{% if answer_set.organisation %}
|
||||
<tr><td>Organisation</td><td>{{ answer_set.organisation }}</td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% if person.disciplines %}
|
||||
<dt>Discipline(s)</dt>
|
||||
<dd>{{ person.disciplines }}</dd>
|
||||
{% endif %}
|
||||
{% if answer_set.organisation_started_date %}
|
||||
<tr><td>Organisation Started Date</td><td>{{ answer_set.organisation_started_date }}</td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% if person.themes.exists %}
|
||||
<dt>Project Themes</dt>
|
||||
<dd>
|
||||
{% for theme in person.themes.all %}
|
||||
{{ theme }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% if answer_set.job_title %}
|
||||
<tr><td>Job Title</td><td>{{ answer_set.job_title }}</td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% if answer_set.disciplines %}
|
||||
<tr><td>Discipline(s)</td><td>{{ answer_set.disciplines }}</td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% if answer_set.themes.exists %}
|
||||
<tr>
|
||||
<td>Project Themes</td>
|
||||
<td>
|
||||
{% for theme in answer_set.themes.all %}
|
||||
{{ theme }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% for answer in answer_set.question_answers.all %}
|
||||
<tr>
|
||||
<td>{{ answer.question }}</td>
|
||||
<td>{{ answer }}</td>
|
||||
</tr>
|
||||
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td>No records</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Last updated: {{ answer_set.timestamp }}</p>
|
||||
{% endwith %}
|
||||
|
||||
<a class="btn btn-success"
|
||||
href="{% url 'people:person.update' pk=person.pk %}">Update</a>
|
||||
|
||||
{% if person.user == request.user %}
|
||||
<a class="btn btn-info"
|
||||
href="{% url 'password_change' %}?next={{ person.get_absolute_url }}">Change Password</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-success"
|
||||
href="{% url 'people:person.update' pk=person.pk %}">Update</a>
|
||||
|
||||
<a class="btn btn-info"
|
||||
href="{% url 'password_change' %}?next={{ person.get_absolute_url }}">Change Password</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -11,10 +11,8 @@
|
||||
|
||||
<hr>
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
<a class="btn btn-success"
|
||||
href="{% url 'people:person.create' %}">New Person</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-success"
|
||||
href="{% url 'people:person.create' %}">New Person</a>
|
||||
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
|
||||
@@ -7,3 +7,10 @@ from . import (
|
||||
person,
|
||||
relationship
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'network',
|
||||
'person',
|
||||
'relationship',
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@ Views for displaying or manipulating instances of :class:`Person`.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.utils import timezone
|
||||
from django.views.generic import CreateView, DetailView, ListView, UpdateView
|
||||
|
||||
from people import forms, models, permissions
|
||||
@@ -55,9 +56,40 @@ class ProfileView(permissions.UserIsLinkedPersonMixin, DetailView):
|
||||
|
||||
|
||||
class PersonUpdateView(permissions.UserIsLinkedPersonMixin, UpdateView):
|
||||
"""
|
||||
View for updating a :class:`Person` record.
|
||||
"""
|
||||
model = models.Person
|
||||
"""View for updating a :class:`Person` record."""
|
||||
model = models.PersonAnswerSet
|
||||
template_name = 'people/person/update.html'
|
||||
form_class = forms.PersonForm
|
||||
form_class = forms.PersonAnswerSetForm
|
||||
|
||||
def get_test_person(self) -> models.Person:
|
||||
"""Get the person instance which should be used for access control checks."""
|
||||
return models.Person.objects.get(pk=self.kwargs.get('pk'))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.person = models.Person.objects.get(pk=self.kwargs.get('pk'))
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.person = models.Person.objects.get(pk=self.kwargs.get('pk'))
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['person'] = self.person
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Mark any previous answer sets as replaced."""
|
||||
response = super().form_valid(form)
|
||||
now_date = timezone.now().date()
|
||||
|
||||
# Shouldn't be more than one after initial updates after migration
|
||||
for answer_set in self.person.answer_sets.exclude(pk=self.object.pk):
|
||||
answer_set.replaced_timestamp = now_date
|
||||
answer_set.save()
|
||||
|
||||
return response
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Views for displaying or manipulating instances of :class:`Relationship`.
|
||||
"""
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.forms import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.generic import CreateView, DetailView
|
||||
from django.views.generic import CreateView, DetailView, FormView
|
||||
|
||||
from people import forms, models, permissions
|
||||
|
||||
@@ -18,7 +20,7 @@ class RelationshipDetailView(permissions.UserIsLinkedPersonMixin, DetailView):
|
||||
related_person_field = 'source'
|
||||
|
||||
|
||||
class RelationshipCreateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||
class RelationshipCreateView(permissions.UserIsLinkedPersonMixin, FormView):
|
||||
"""
|
||||
View for creating a :class:`Relationship`.
|
||||
|
||||
@@ -26,53 +28,45 @@ class RelationshipCreateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||
"""
|
||||
model = models.Relationship
|
||||
template_name = 'people/relationship/create.html'
|
||||
fields = [
|
||||
'source',
|
||||
'target',
|
||||
]
|
||||
|
||||
def get_test_person(self) -> models.Person:
|
||||
"""
|
||||
Get the person instance which should be used for access control checks.
|
||||
"""
|
||||
if self.request.method == 'POST':
|
||||
return models.Person.objects.get(pk=self.request.POST.get('source'))
|
||||
form_class = forms.RelationshipForm
|
||||
|
||||
def get_person(self) -> models.Person:
|
||||
return models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.person = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||
def get_test_person(self) -> models.Person:
|
||||
return self.get_person()
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
self.object = models.Relationship.objects.create(
|
||||
source=self.get_person(), target=form.cleaned_data['target'])
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.person = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||
except IntegrityError:
|
||||
form.add_error(
|
||||
None,
|
||||
ValidationError('This relationship already exists',
|
||||
code='already-exists'))
|
||||
return self.form_invalid(form)
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
|
||||
initial['source'] = self.request.user.person
|
||||
initial['target'] = self.person
|
||||
|
||||
return initial
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['person'] = self.person
|
||||
context['person'] = self.get_person()
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('people:relationship.update', kwargs={'relationship_pk': self.object.pk})
|
||||
return reverse('people:relationship.update',
|
||||
kwargs={'relationship_pk': self.object.pk})
|
||||
|
||||
|
||||
class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||
"""
|
||||
View for creating a :class:`Relationship`.
|
||||
View for updating the details of a relationship.
|
||||
|
||||
Creates a new :class:`RelationshipAnswerSet` for the :class:`Relationship`.
|
||||
Displays / processes a form containing the :class:`RelationshipQuestion`s.
|
||||
"""
|
||||
model = models.RelationshipAnswerSet
|
||||
@@ -83,18 +77,21 @@ class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||
"""
|
||||
Get the person instance which should be used for access control checks.
|
||||
"""
|
||||
relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
||||
relationship = models.Relationship.objects.get(
|
||||
pk=self.kwargs.get('relationship_pk'))
|
||||
|
||||
return relationship.source
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
||||
self.relationship = models.Relationship.objects.get(
|
||||
pk=self.kwargs.get('relationship_pk'))
|
||||
self.person = self.relationship.source
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
||||
self.relationship = models.Relationship.objects.get(
|
||||
pk=self.kwargs.get('relationship_pk'))
|
||||
self.person = self.relationship.source
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
@@ -122,7 +119,8 @@ class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||
now_date = timezone.now().date()
|
||||
|
||||
# Shouldn't be more than one after initial updates after migration
|
||||
for answer_set in self.relationship.answer_sets.exclude(pk=self.object.pk):
|
||||
for answer_set in self.relationship.answer_sets.exclude(
|
||||
pk=self.object.pk):
|
||||
answer_set.replaced_timestamp = now_date
|
||||
answer_set.save()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user