Merge pull request #69 from Southampton-RSG/dev

State as of demo 2021-02-02
This commit is contained in:
James Graham
2021-02-02 09:05:27 +00:00
committed by GitHub
26 changed files with 409 additions and 239 deletions

15
breccia_mapper/forms.py Normal file
View File

@@ -0,0 +1,15 @@
from django import forms
from django.contrib.auth import get_user_model
User = get_user_model() # pylint: disable=invalid-name
class ConsentForm(forms.ModelForm):
"""Form used to collect user consent for data collection / processing."""
class Meta:
model = User
fields = ['consent_given']
labels = {
'consent_given':
'I have read and understood this information and consent to my data being used in this way',
}

View File

@@ -157,6 +157,7 @@ THIRD_PARTY_APPS = [
'django_select2', 'django_select2',
'rest_framework', 'rest_framework',
'post_office', 'post_office',
'bootstrap_datepicker_plus',
] ]
FIRST_PARTY_APPS = [ FIRST_PARTY_APPS = [
@@ -327,7 +328,7 @@ LOGGING = {
LOGGING_CONFIG = None LOGGING_CONFIG = None
logging.config.dictConfig(LOGGING) logging.config.dictConfig(LOGGING)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__) # pylint: disable=invalid-name
# Admin panel variables # Admin panel variables
@@ -337,10 +338,17 @@ CONSTANCE_CONFIG = collections.OrderedDict([
'Text to be displayed in a notice banner at the top of every page.')), 'Text to be displayed in a notice banner at the top of every page.')),
('NOTICE_CLASS', ('alert-warning', ('NOTICE_CLASS', ('alert-warning',
'CSS class to use for background of notice banner.')), 'CSS class to use for background of notice banner.')),
('CONSENT_TEXT',
('This is template consent text and should have been replaced. Please contact an admin.',
'Text to be displayed to ask for consent for data collection.'))
]) ])
CONSTANCE_CONFIG_FIELDSETS = { CONSTANCE_CONFIG_FIELDSETS = {
'Notice Banner': ('NOTICE_TEXT', 'NOTICE_CLASS'), 'Notice Banner': (
'NOTICE_TEXT',
'NOTICE_CLASS',
),
'Data Collection': ('CONSENT_TEXT', ),
} }
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
@@ -379,12 +387,10 @@ else:
default=(EMAIL_PORT == 465), default=(EMAIL_PORT == 465),
cast=bool) cast=bool)
# Upstream API keys # Upstream API keys
GOOGLE_MAPS_API_KEY = config('GOOGLE_MAPS_API_KEY', default=None) GOOGLE_MAPS_API_KEY = config('GOOGLE_MAPS_API_KEY', default=None)
# Import customisation app settings if present # Import customisation app settings if present
try: try:

View File

@@ -156,6 +156,15 @@
</div> </div>
{% endif %} {% endif %}
{% if request.user.is_authenticated and not request.user.consent_given %}
<div class="alert alert-warning rounded-0" role="alert">
<p class="text-center mb-0">
You have not yet given consent for your data to be collected and processed.
Please read and accept the <a href="{% url 'consent' %}">consent text</a>.
</p>
</div>
{% endif %}
{% block before_content %}{% endblock %} {% block before_content %}{% endblock %}
<main class="container"> <main class="container">

View File

@@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block content %}
<h2>Consent</h2>
<p>
{{ config.CONSENT_TEXT|linebreaks }}
</p>
<form class="form"
method="POST">
{% csrf_token %}
{% load bootstrap4 %}
{% bootstrap_form form %}
{% buttons %}
<button class="btn btn-success" type="submit">Submit</button>
{% endbuttons %}
</form>
{% endblock %}

View File

@@ -63,7 +63,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row align-items-center"> <div class="row align-items-center" style="min-height: 400px;">
<div class="col-sm-8"> <div class="col-sm-8">
<h2 class="pb-2">About {{ settings.PROJECT_LONG_NAME }}</h2> <h2 class="pb-2">About {{ settings.PROJECT_LONG_NAME }}</h2>

View File

@@ -32,6 +32,10 @@ urlpatterns = [
views.IndexView.as_view(), views.IndexView.as_view(),
name='index'), name='index'),
path('consent',
views.ConsentTextView.as_view(),
name='consent'),
path('', path('',
include('export.urls')), include('export.urls')),

View File

@@ -1,13 +1,31 @@
""" """Views belonging to the core of the project.
Views belonging to the core of the project.
These views don't represent any of the models in the apps. These views don't represent any of the models in the apps.
""" """
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.edit import UpdateView
from . import forms
User = get_user_model() # pylint: disable=invalid-name
class IndexView(TemplateView): class IndexView(TemplateView):
# Template set in Django settings file - may be customised by a customisation app # Template set in Django settings file - may be customised by a customisation app
template_name = settings.TEMPLATE_NAME_INDEX template_name = settings.TEMPLATE_NAME_INDEX
class ConsentTextView(LoginRequiredMixin, UpdateView):
"""View with consent text and form for users to indicate consent."""
model = User
form_class = forms.ConsentForm
template_name = 'consent.html'
success_url = reverse_lazy('index')
def get_object(self, *args, **kwargs) -> User:
return self.request.user

View File

@@ -3,25 +3,13 @@
import typing import typing
from django import forms from django import forms
from django.forms.widgets import SelectDateWidget
from django.utils import timezone
from bootstrap_datepicker_plus import DatePickerInput
from django_select2.forms import ModelSelect2Widget, Select2Widget, Select2MultipleWidget from django_select2.forms import ModelSelect2Widget, Select2Widget, Select2MultipleWidget
from . import models from . import models
def get_date_year_range() -> typing.Iterable[int]:
"""
Get sensible year range for SelectDateWidgets in the past.
By default these widgets show 10 years in the future.
"""
num_years_display = 60
this_year = timezone.datetime.now().year
return range(this_year, this_year - num_years_display, -1)
class OrganisationForm(forms.ModelForm): class OrganisationForm(forms.ModelForm):
"""Form for creating / updating an instance of :class:`Organisation`.""" """Form for creating / updating an instance of :class:`Organisation`."""
class Meta: class Meta:
@@ -46,9 +34,10 @@ class RelationshipForm(forms.Form):
class DynamicAnswerSetBase(forms.Form): class DynamicAnswerSetBase(forms.Form):
field_class = forms.ModelChoiceField field_class = forms.ModelChoiceField
field_widget = None
field_required = True field_required = True
question_model = None field_widget: typing.Optional[typing.Type[forms.Widget]] = None
question_model: typing.Type[models.Question]
answer_model: typing.Type[models.QuestionChoice]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -72,6 +61,11 @@ class DynamicAnswerSetBase(forms.Form):
initial=initial.get(field_name, None)) initial=initial.get(field_name, None))
self.fields[field_name] = field self.fields[field_name] = field
if question.allow_free_text:
free_field = forms.CharField(label=f'{question} free text',
required=False)
self.fields[f'{field_name}_free'] = free_field
class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase): class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
"""Form for variable person attributes. """Form for variable person attributes.
@@ -94,6 +88,7 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
widgets = { widgets = {
'nationality': Select2Widget(), 'nationality': Select2Widget(),
'country_of_residence': Select2Widget(), 'country_of_residence': Select2Widget(),
'organisation_started_date': DatePickerInput(format='%Y-%m-%d'),
'themes': Select2MultipleWidget(), 'themes': Select2MultipleWidget(),
'latitude': forms.HiddenInput, 'latitude': forms.HiddenInput,
'longitude': forms.HiddenInput, 'longitude': forms.HiddenInput,
@@ -104,6 +99,7 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
} }
question_model = models.PersonQuestion question_model = models.PersonQuestion
answer_model = models.PersonQuestionChoice
def save(self, commit=True) -> models.PersonAnswerSet: def save(self, commit=True) -> models.PersonAnswerSet:
# Save Relationship model # Save Relationship model
@@ -116,6 +112,13 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
# Save answers to relationship questions # Save answers to relationship questions
for key, value in self.cleaned_data.items(): for key, value in self.cleaned_data.items():
if key.startswith('question_') and value: if key.startswith('question_') and value:
if key.endswith('_free'):
# Create new answer from free text
value, _ = self.answer_model.objects.get_or_create(
text=value,
question=self.question_model.objects.get(
pk=key.split('_')[1]))
try: try:
self.instance.question_answers.add(value) self.instance.question_answers.add(value)
@@ -139,6 +142,7 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
] ]
question_model = models.RelationshipQuestion question_model = models.RelationshipQuestion
answer_model = models.RelationshipQuestionChoice
def save(self, commit=True) -> models.RelationshipAnswerSet: def save(self, commit=True) -> models.RelationshipAnswerSet:
# Save Relationship model # Save Relationship model
@@ -148,6 +152,13 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
# Save answers to relationship questions # Save answers to relationship questions
for key, value in self.cleaned_data.items(): for key, value in self.cleaned_data.items():
if key.startswith('question_') and value: if key.startswith('question_') and value:
if key.endswith('_free'):
# Create new answer from free text
value, _ = self.answer_model.objects.get_or_create(
text=value,
question=self.question_model.objects.get(
pk=key.split('_')[1]))
try: try:
self.instance.question_answers.add(value) self.instance.question_answers.add(value)
@@ -166,6 +177,7 @@ class NetworkFilterForm(DynamicAnswerSetBase):
field_widget = Select2MultipleWidget field_widget = Select2MultipleWidget
field_required = False field_required = False
question_model = models.RelationshipQuestion question_model = models.RelationshipQuestion
answer_model = models.RelationshipQuestionChoice
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -173,5 +185,5 @@ class NetworkFilterForm(DynamicAnswerSetBase):
# Add date field to select relationships at a particular point in time # Add date field to select relationships at a particular point in time
self.fields['date'] = forms.DateField( self.fields['date'] = forms.DateField(
required=False, required=False,
widget=SelectDateWidget(years=get_date_year_range()), widget=DatePickerInput(format='%Y-%m-%d'),
help_text='Show relationships as they were on this date') help_text='Show relationships as they were on this date')

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2021-01-20 11:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('people', '0029_organisation_location_fields'),
]
operations = [
migrations.AddField(
model_name='user',
name='consent_given',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.10 on 2021-01-20 13:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('people', '0030_user_consent_given'),
]
operations = [
migrations.AddField(
model_name='personquestion',
name='allow_free_text',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='relationshipquestion',
name='allow_free_text',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,2 +1,3 @@
from .person import * from .person import *
from .question import *
from .relationship import * from .relationship import *

View File

@@ -32,6 +32,9 @@ class User(AbstractUser):
""" """
email = models.EmailField(_('email address'), blank=False, null=False) email = models.EmailField(_('email address'), blank=False, null=False)
#: Have they given consent to collect and store their data?
consent_given = models.BooleanField(default=False)
def has_person(self) -> bool: def has_person(self) -> bool:
""" """
Does this user have a linked :class:`Person` record? Does this user have a linked :class:`Person` record?

View File

@@ -4,6 +4,11 @@ import typing
from django.db import models from django.db import models
from django.utils.text import slugify from django.utils.text import slugify
__all__ = [
'Question',
'QuestionChoice',
]
class Question(models.Model): class Question(models.Model):
"""Questions from which a survey form can be created.""" """Questions from which a survey form can be created."""
@@ -27,6 +32,11 @@ class Question(models.Model):
blank=False, blank=False,
null=False) null=False)
#: Should people be able to add their own answers?
allow_free_text = models.BooleanField(default=False,
blank=False,
null=False)
#: Position of this question in the list #: Position of this question in the list
order = models.SmallIntegerField(default=0, blank=False, null=False) order = models.SmallIntegerField(default=0, blank=False, null=False)

View File

@@ -1,5 +1,4 @@
let marker = null;
let search_markers = [] let search_markers = []
/** /**
@@ -7,9 +6,9 @@ let search_markers = []
* @param {Event} event - Click event from a Google Map. * @param {Event} event - Click event from a Google Map.
*/ */
function selectLocation(event) { function selectLocation(event) {
if (marker === null) { if (selected_marker === null) {
// Generate a new marker // Generate a new marker
marker = new google.maps.Marker({ selected_marker = new google.maps.Marker({
position: event.latLng, position: event.latLng,
map: map, map: map,
icon: { icon: {
@@ -24,10 +23,10 @@ function selectLocation(event) {
}, },
}); });
} else { } else {
marker.setPosition(event.latLng); selected_marker.setPosition(event.latLng);
} }
const pos = marker.getPosition(); const pos = selected_marker.getPosition();
document.getElementById('id_latitude').value = pos.lat(); document.getElementById('id_latitude').value = pos.lat();
document.getElementById('id_longitude').value = pos.lng(); document.getElementById('id_longitude').value = pos.lng();
} }

View File

@@ -11,6 +11,7 @@ const marker_edge_alpha = 1.0;
const marker_edge_width = 1.0; const marker_edge_width = 1.0;
let map = null; let map = null;
let selected_marker = null;
let selected_marker_info = null; let selected_marker_info = null;
function createMarker(map, marker_data) { function createMarker(map, marker_data) {
@@ -75,6 +76,9 @@ function initMap() {
try { try {
const marker = createMarker(map, marker_data); const marker = createMarker(map, marker_data);
bounds.extend(marker.position); bounds.extend(marker.position);
if (markers_data.length === 1) {
selected_marker = marker;
}
} catch (exc) { } catch (exc) {
// Just skip and move on to next // Just skip and move on to next

View File

@@ -1,163 +0,0 @@
{% extends 'base.html' %}
{% block extra_head %}
{{ map_markers|json_script:'map-markers' }}
{% load staticfiles %}
<script src="{% static 'js/map.js' %}"></script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap"
type="text/javascript"></script>
{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'people:person.list' %}">People</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ object }}</li>
</ol>
</nav>
<h1>{{ person.name }}</h1>
<hr>
{% if person.user == request.user or request.user.is_superuser %}
{% if person.user != request.user and request.user.is_superuser %}
<div class="alert alert-warning">
<strong>NB:</strong> You are able to see the details of this person because you are an admin.
Regular users are not able to see this information for people other than themselves.
</div>
{% endif %}
{% include 'people/person/includes/answer_set.html' %}
<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 %}
<hr>
<div id="map" style="height: 800px; width: 100%"></div>
<hr>
<div class="row">
<div class="col-md-6">
<h2>Relationships As Source</h2>
<table class="table table-borderless">
<thead>
<tr>
<th>Contact Name</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for relationship in person.relationships_as_source.all %}
<tr>
<td>{{ relationship.target }}</td>
<td>
<a class="btn btn-sm btn-info"
href="{% url 'people:person.detail' pk=relationship.target.pk %}">Profile</a>
</td>
<td>
<a class="btn btn-sm btn-info"
href="{% url 'people:relationship.detail' pk=relationship.pk %}">Relationship Detail</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">No known relationships</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="btn btn-success btn-block"
href="{% url 'people:person.relationship.create' person_pk=person.pk %}">New Relationship
</a>
</div>
<div class="col-md-6">
<h2>Relationships As Target</h2>
<table class="table table-borderless">
<thead>
<tr>
<th>Contact Name</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for relationship in person.relationships_as_target.all %}
<tr>
<td>{{ relationship.source }}</td>
<td>
<a class="btn btn-sm btn-info"
href="{% url 'people:person.detail' pk=relationship.source.pk %}">Profile</a>
</td>
<td>
<a class="btn btn-sm btn-info"
href="{% url 'people:relationship.detail' pk=relationship.pk %}">Relationship Detail</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">No known relationships</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<hr>
<h2>Activities</h2>
<table class="table table-borderless">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for activity in person.activities.all %}
<tr>
<td>{{ activity }}</td>
<td>
<a class="btn btn-sm btn-info"
href="{% url 'activities:activity.detail' pk=activity.pk %}">Details</a>
</td>
</tr>
{% empty %}
<tr>
<td>No records</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block extra_script %}
{% endblock %}

View File

@@ -0,0 +1,59 @@
{% extends 'base.html' %}
{% block extra_head %}
{{ map_markers|json_script:'map-markers' }}
{% load staticfiles %}
<script src="{% static 'js/map.js' %}"></script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap"
type="text/javascript"></script>
{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'people:person.list' %}">People</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ object }}</li>
</ol>
</nav>
<h1>{{ person.name }}</h1>
<hr>
{% if person.user != request.user and request.user.is_superuser %}
<div class="alert alert-warning">
<strong>NB:</strong> You are able to see the details of this person because you are an admin.
Regular users are not able to see this information for people other than themselves.
</div>
{% endif %}
{% include 'people/person/includes/answer_set_full.html' %}
<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 %}
<hr>
<div id="map" style="height: 800px; width: 100%"></div>
<hr>
{% include 'people/person/includes/relationships_full.html' %}
<hr>
{% include 'people/person/includes/activities_full.html' %}
<hr>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends 'base.html' %}
{% block extra_head %}
{{ map_markers|json_script:'map-markers' }}
{% load staticfiles %}
<script src="{% static 'js/map.js' %}"></script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap"
type="text/javascript"></script>
{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'people:person.list' %}">People</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ object }}</li>
</ol>
</nav>
<h1>{{ person.name }}</h1>
<a class="btn btn-success"
href="{% url 'people:person.relationship.create' person_pk=person.pk %}">New Relationship
</a>
<hr>
{% include 'people/person/includes/answer_set_partial.html' %}
<hr>
<div id="map" style="height: 800px; width: 100%"></div>
<hr>
{% endblock %}

View File

@@ -0,0 +1,26 @@
<h2>Activities</h2>
<table class="table table-borderless">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for activity in person.activities.all %}
<tr>
<td>{{ activity }}</td>
<td>
<a class="btn btn-sm btn-info"
href="{% url 'activities:activity.detail' pk=activity.pk %}">Details</a>
</td>
</tr>
{% empty %}
<tr>
<td>No records</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -0,0 +1,32 @@
<table class="table table-borderless">
<thead>
<tr>
<th>Question</th>
<th>Answer</th>
</tr>
</thead>
<tbody>
{% if answer_set.country_of_residence %}
<tr><td>Country of Residence</td><td>{{ answer_set.country_of_residence.name }}</td></tr>
{% endif %}
{% if answer_set.organisation %}
<tr><td>Organisation</td><td>{{ answer_set.organisation }}</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>

View File

@@ -0,0 +1,36 @@
<h2>People I've Answered Questions About</h2>
<table class="table table-borderless">
<thead>
<tr>
<th>Contact Name</th>
<th></th>
</tr>
</thead>
<tbody>
{% for relationship in person.relationships_as_source.all %}
<tr>
<td>{{ relationship.target }}</td>
<td>
<a class="btn btn-sm btn-info"
href="{% url 'people:person.detail' pk=relationship.target.pk %}">Profile</a>
<a class="btn btn-sm btn-info"
href="{% url 'people:relationship.detail' pk=relationship.pk %}">Relationship Detail</a>
<a class="btn btn-sm btn-success"
href="{% url 'people:relationship.update' relationship_pk=relationship.pk %}">Update</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">No known relationships</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="btn btn-success"
href="{% url 'people:person.relationship.create' person_pk=person.pk %}">New Relationship
</a>

View File

@@ -28,6 +28,19 @@
<td> <td>
<a class="btn btn-sm btn-info" <a class="btn btn-sm btn-info"
href="{% url 'people:person.detail' pk=person.pk %}">Profile</a> href="{% url 'people:person.detail' pk=person.pk %}">Profile</a>
{% if person.pk in existing_relationships %}
<a class="btn btn-sm btn-warning"
style="width: 10rem"
href="{% url 'people:person.relationship.create' person_pk=person.pk %}">Update Relationship
</a>
{% else %}
<a class="btn btn-sm btn-success"
style="width: 10rem"
href="{% url 'people:person.relationship.create' person_pk=person.pk %}">New Relationship
</a>
{% endif %}
</td> </td>
</tr> </tr>

View File

@@ -30,19 +30,33 @@ class PersonCreateView(LoginRequiredMixin, CreateView):
class PersonListView(LoginRequiredMixin, ListView): class PersonListView(LoginRequiredMixin, ListView):
""" """View displaying a list of :class:`Person` objects - searchable."""
View displaying a list of :class:`Person` objects - searchable.
"""
model = models.Person model = models.Person
template_name = 'people/person/list.html' template_name = 'people/person/list.html'
def get_context_data(self,
**kwargs: typing.Any) -> typing.Dict[str, typing.Any]:
context = super().get_context_data(**kwargs)
class ProfileView(permissions.UserIsLinkedPersonMixin, DetailView): context['existing_relationships'] = set(
self.request.user.person.relationship_targets.values_list(
'pk', flat=True))
return context
class ProfileView(LoginRequiredMixin, DetailView):
""" """
View displaying the profile of a :class:`Person` - who may be a user. View displaying the profile of a :class:`Person` - who may be a user.
""" """
model = models.Person model = models.Person
template_name = 'people/person/detail.html'
def get_template_names(self) -> typing.List[str]:
"""Return template depending on level of access."""
if (self.object.user == self.request.user) or self.request.user.is_superuser:
return ['people/person/detail_full.html']
return ['people/person/detail_partial.html']
def get_object(self, queryset=None) -> models.Person: def get_object(self, queryset=None) -> models.Person:
""" """

View File

@@ -1,12 +1,11 @@
""" """Views for displaying or manipulating instances of :class:`Relationship`."""
Views for displaying or manipulating instances of :class:`Relationship`.
"""
from django.db import IntegrityError import typing
from django.forms import ValidationError
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.views.generic import CreateView, DetailView, FormView from django.views.generic import CreateView, DetailView, RedirectView
from people import forms, models, permissions from people import forms, models, permissions
@@ -20,46 +19,18 @@ class RelationshipDetailView(permissions.UserIsLinkedPersonMixin, DetailView):
related_person_field = 'source' related_person_field = 'source'
class RelationshipCreateView(permissions.UserIsLinkedPersonMixin, FormView): class RelationshipCreateView(LoginRequiredMixin, RedirectView):
"""View for creating a :class:`Relationship`.
Redirects to a form containing the :class:`RelationshipQuestion`s.
""" """
View for creating a :class:`Relationship`. def get_redirect_url(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Optional[str]:
target = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
relationship, _ = models.Relationship.objects.get_or_create(
source=self.request.user.person, target=target)
Displays / processes a form containing the :class:`RelationshipQuestion`s.
"""
model = models.Relationship
template_name = 'people/relationship/create.html'
form_class = forms.RelationshipForm
def get_person(self) -> models.Person:
return models.Person.objects.get(pk=self.kwargs.get('person_pk'))
def get_test_person(self) -> models.Person:
return self.get_person()
def form_valid(self, form):
try:
self.object = models.Relationship.objects.create(
source=self.get_person(), target=form.cleaned_data['target'])
except IntegrityError:
form.add_error(
None,
ValidationError('This relationship already exists',
code='already-exists'))
return self.form_invalid(form)
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['person'] = self.get_person()
return context
def get_success_url(self):
return reverse('people:relationship.update', return reverse('people:relationship.update',
kwargs={'relationship_pk': self.object.pk}) kwargs={'relationship_pk': relationship.pk})
class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView): class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):

View File

@@ -4,6 +4,7 @@ dj-database-url==0.5.0
Django==2.2.10 Django==2.2.10
django-appconf==1.0.3 django-appconf==1.0.3
django-bootstrap4==1.1.1 django-bootstrap4==1.1.1
django-bootstrap-datepicker-plus==3.0.5
django-constance==2.6.0 django-constance==2.6.0
django-countries==5.5 django-countries==5.5
django-dbbackup==3.2.0 django-dbbackup==3.2.0