mirror of
https://github.com/Southampton-RSG/breccia-mapper.git
synced 2026-03-02 19:07:06 +00:00
Merge pull request #69 from Southampton-RSG/dev
State as of demo 2021-02-02
This commit is contained in:
15
breccia_mapper/forms.py
Normal file
15
breccia_mapper/forms.py
Normal 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',
|
||||
}
|
||||
@@ -157,6 +157,7 @@ THIRD_PARTY_APPS = [
|
||||
'django_select2',
|
||||
'rest_framework',
|
||||
'post_office',
|
||||
'bootstrap_datepicker_plus',
|
||||
]
|
||||
|
||||
FIRST_PARTY_APPS = [
|
||||
@@ -327,7 +328,7 @@ LOGGING = {
|
||||
|
||||
LOGGING_CONFIG = None
|
||||
logging.config.dictConfig(LOGGING)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
# 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.')),
|
||||
('NOTICE_CLASS', ('alert-warning',
|
||||
'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 = {
|
||||
'Notice Banner': ('NOTICE_TEXT', 'NOTICE_CLASS'),
|
||||
'Notice Banner': (
|
||||
'NOTICE_TEXT',
|
||||
'NOTICE_CLASS',
|
||||
),
|
||||
'Data Collection': ('CONSENT_TEXT', ),
|
||||
}
|
||||
|
||||
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
|
||||
@@ -379,12 +387,10 @@ else:
|
||||
default=(EMAIL_PORT == 465),
|
||||
cast=bool)
|
||||
|
||||
|
||||
# Upstream API keys
|
||||
|
||||
GOOGLE_MAPS_API_KEY = config('GOOGLE_MAPS_API_KEY', default=None)
|
||||
|
||||
|
||||
# Import customisation app settings if present
|
||||
|
||||
try:
|
||||
|
||||
@@ -156,6 +156,15 @@
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
<main class="container">
|
||||
|
||||
21
breccia_mapper/templates/consent.html
Normal file
21
breccia_mapper/templates/consent.html
Normal 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 %}
|
||||
@@ -63,7 +63,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row align-items-center">
|
||||
<div class="row align-items-center" style="min-height: 400px;">
|
||||
<div class="col-sm-8">
|
||||
<h2 class="pb-2">About {{ settings.PROJECT_LONG_NAME }}</h2>
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ urlpatterns = [
|
||||
views.IndexView.as_view(),
|
||||
name='index'),
|
||||
|
||||
path('consent',
|
||||
views.ConsentTextView.as_view(),
|
||||
name='consent'),
|
||||
|
||||
path('',
|
||||
include('export.urls')),
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
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.edit import UpdateView
|
||||
|
||||
from . import forms
|
||||
|
||||
User = get_user_model() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class IndexView(TemplateView):
|
||||
# Template set in Django settings file - may be customised by a customisation app
|
||||
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
|
||||
|
||||
@@ -3,25 +3,13 @@
|
||||
import typing
|
||||
|
||||
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 . 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):
|
||||
"""Form for creating / updating an instance of :class:`Organisation`."""
|
||||
class Meta:
|
||||
@@ -46,9 +34,10 @@ class RelationshipForm(forms.Form):
|
||||
|
||||
class DynamicAnswerSetBase(forms.Form):
|
||||
field_class = forms.ModelChoiceField
|
||||
field_widget = None
|
||||
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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -72,6 +61,11 @@ class DynamicAnswerSetBase(forms.Form):
|
||||
initial=initial.get(field_name, None))
|
||||
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):
|
||||
"""Form for variable person attributes.
|
||||
@@ -94,6 +88,7 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
widgets = {
|
||||
'nationality': Select2Widget(),
|
||||
'country_of_residence': Select2Widget(),
|
||||
'organisation_started_date': DatePickerInput(format='%Y-%m-%d'),
|
||||
'themes': Select2MultipleWidget(),
|
||||
'latitude': forms.HiddenInput,
|
||||
'longitude': forms.HiddenInput,
|
||||
@@ -104,6 +99,7 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
}
|
||||
|
||||
question_model = models.PersonQuestion
|
||||
answer_model = models.PersonQuestionChoice
|
||||
|
||||
def save(self, commit=True) -> models.PersonAnswerSet:
|
||||
# Save Relationship model
|
||||
@@ -116,6 +112,13 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
# Save answers to relationship questions
|
||||
for key, value in self.cleaned_data.items():
|
||||
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:
|
||||
self.instance.question_answers.add(value)
|
||||
|
||||
@@ -139,6 +142,7 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
]
|
||||
|
||||
question_model = models.RelationshipQuestion
|
||||
answer_model = models.RelationshipQuestionChoice
|
||||
|
||||
def save(self, commit=True) -> models.RelationshipAnswerSet:
|
||||
# Save Relationship model
|
||||
@@ -148,6 +152,13 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
# Save answers to relationship questions
|
||||
for key, value in self.cleaned_data.items():
|
||||
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:
|
||||
self.instance.question_answers.add(value)
|
||||
|
||||
@@ -166,6 +177,7 @@ class NetworkFilterForm(DynamicAnswerSetBase):
|
||||
field_widget = Select2MultipleWidget
|
||||
field_required = False
|
||||
question_model = models.RelationshipQuestion
|
||||
answer_model = models.RelationshipQuestionChoice
|
||||
|
||||
def __init__(self, *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
|
||||
self.fields['date'] = forms.DateField(
|
||||
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')
|
||||
|
||||
18
people/migrations/0030_user_consent_given.py
Normal file
18
people/migrations/0030_user_consent_given.py
Normal 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),
|
||||
),
|
||||
]
|
||||
23
people/migrations/0031_question_allow_free_text.py
Normal file
23
people/migrations/0031_question_allow_free_text.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -1,2 +1,3 @@
|
||||
from .person import *
|
||||
from .question import *
|
||||
from .relationship import *
|
||||
|
||||
@@ -32,6 +32,9 @@ class User(AbstractUser):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Does this user have a linked :class:`Person` record?
|
||||
|
||||
@@ -4,6 +4,11 @@ import typing
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
|
||||
__all__ = [
|
||||
'Question',
|
||||
'QuestionChoice',
|
||||
]
|
||||
|
||||
|
||||
class Question(models.Model):
|
||||
"""Questions from which a survey form can be created."""
|
||||
@@ -27,6 +32,11 @@ class Question(models.Model):
|
||||
blank=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
|
||||
order = models.SmallIntegerField(default=0, blank=False, null=False)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
let marker = null;
|
||||
let search_markers = []
|
||||
|
||||
/**
|
||||
@@ -7,9 +6,9 @@ let search_markers = []
|
||||
* @param {Event} event - Click event from a Google Map.
|
||||
*/
|
||||
function selectLocation(event) {
|
||||
if (marker === null) {
|
||||
if (selected_marker === null) {
|
||||
// Generate a new marker
|
||||
marker = new google.maps.Marker({
|
||||
selected_marker = new google.maps.Marker({
|
||||
position: event.latLng,
|
||||
map: map,
|
||||
icon: {
|
||||
@@ -24,10 +23,10 @@ function selectLocation(event) {
|
||||
},
|
||||
});
|
||||
} 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_longitude').value = pos.lng();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const marker_edge_alpha = 1.0;
|
||||
const marker_edge_width = 1.0;
|
||||
|
||||
let map = null;
|
||||
let selected_marker = null;
|
||||
let selected_marker_info = null;
|
||||
|
||||
function createMarker(map, marker_data) {
|
||||
@@ -75,6 +76,9 @@ function initMap() {
|
||||
try {
|
||||
const marker = createMarker(map, marker_data);
|
||||
bounds.extend(marker.position);
|
||||
if (markers_data.length === 1) {
|
||||
selected_marker = marker;
|
||||
}
|
||||
|
||||
} catch (exc) {
|
||||
// Just skip and move on to next
|
||||
|
||||
@@ -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 %}
|
||||
59
people/templates/people/person/detail_full.html
Normal file
59
people/templates/people/person/detail_full.html
Normal 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 %}
|
||||
38
people/templates/people/person/detail_partial.html
Normal file
38
people/templates/people/person/detail_partial.html
Normal 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 %}
|
||||
26
people/templates/people/person/includes/activities_full.html
Normal file
26
people/templates/people/person/includes/activities_full.html
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -28,6 +28,19 @@
|
||||
<td>
|
||||
<a class="btn btn-sm btn-info"
|
||||
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>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -30,19 +30,33 @@ class PersonCreateView(LoginRequiredMixin, CreateView):
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
from django.forms import ValidationError
|
||||
import typing
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse
|
||||
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
|
||||
|
||||
@@ -20,46 +19,18 @@ class RelationshipDetailView(permissions.UserIsLinkedPersonMixin, DetailView):
|
||||
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',
|
||||
kwargs={'relationship_pk': self.object.pk})
|
||||
kwargs={'relationship_pk': relationship.pk})
|
||||
|
||||
|
||||
class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||
|
||||
@@ -4,6 +4,7 @@ dj-database-url==0.5.0
|
||||
Django==2.2.10
|
||||
django-appconf==1.0.3
|
||||
django-bootstrap4==1.1.1
|
||||
django-bootstrap-datepicker-plus==3.0.5
|
||||
django-constance==2.6.0
|
||||
django-countries==5.5
|
||||
django-dbbackup==3.2.0
|
||||
|
||||
Reference in New Issue
Block a user