Consent
+ ++ {{ config.CONSENT_TEXT|linebreaks }} +
+ + +{% endblock %} diff --git a/breccia_mapper/templates/index.html b/breccia_mapper/templates/index.html index 8f6c0c4..2044dde 100644 --- a/breccia_mapper/templates/index.html +++ b/breccia_mapper/templates/index.html @@ -63,7 +63,7 @@ {% endblock %} {% block content %} -About {{ settings.PROJECT_LONG_NAME }}
diff --git a/breccia_mapper/urls.py b/breccia_mapper/urls.py index 0aade11..4db8da3 100644 --- a/breccia_mapper/urls.py +++ b/breccia_mapper/urls.py @@ -32,6 +32,10 @@ urlpatterns = [ views.IndexView.as_view(), name='index'), + path('consent', + views.ConsentTextView.as_view(), + name='consent'), + path('', include('export.urls')), diff --git a/breccia_mapper/views.py b/breccia_mapper/views.py index ff539f6..44c23f2 100644 --- a/breccia_mapper/views.py +++ b/breccia_mapper/views.py @@ -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 diff --git a/people/forms.py b/people/forms.py index 607214f..2adc9d6 100644 --- a/people/forms.py +++ b/people/forms.py @@ -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') diff --git a/people/migrations/0030_user_consent_given.py b/people/migrations/0030_user_consent_given.py new file mode 100644 index 0000000..e51adfa --- /dev/null +++ b/people/migrations/0030_user_consent_given.py @@ -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), + ), + ] diff --git a/people/migrations/0031_question_allow_free_text.py b/people/migrations/0031_question_allow_free_text.py new file mode 100644 index 0000000..a1dfaff --- /dev/null +++ b/people/migrations/0031_question_allow_free_text.py @@ -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), + ), + ] diff --git a/people/models/__init__.py b/people/models/__init__.py index e8bc10e..f3a1059 100644 --- a/people/models/__init__.py +++ b/people/models/__init__.py @@ -1,2 +1,3 @@ from .person import * +from .question import * from .relationship import * diff --git a/people/models/person.py b/people/models/person.py index 670f0db..914c53d 100644 --- a/people/models/person.py +++ b/people/models/person.py @@ -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? diff --git a/people/models/question.py b/people/models/question.py index 7379fb3..7948640 100644 --- a/people/models/question.py +++ b/people/models/question.py @@ -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) diff --git a/people/static/js/location_picker.js b/people/static/js/location_picker.js index fc15d4d..188644b 100644 --- a/people/static/js/location_picker.js +++ b/people/static/js/location_picker.js @@ -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(); } diff --git a/people/static/js/map.js b/people/static/js/map.js index 5945c99..905601e 100644 --- a/people/static/js/map.js +++ b/people/static/js/map.js @@ -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 diff --git a/people/templates/people/person/detail.html b/people/templates/people/person/detail.html deleted file mode 100644 index b071b83..0000000 --- a/people/templates/people/person/detail.html +++ /dev/null @@ -1,163 +0,0 @@ -{% extends 'base.html' %} - -{% block extra_head %} - {{ map_markers|json_script:'map-markers' }} - - {% load staticfiles %} - - - -{% endblock %} - -{% block content %} - - -{{ person.name }}
- -- - {% if person.user == request.user or request.user.is_superuser %} - {% if person.user != request.user and request.user.is_superuser %} -
- - - -
- -
Relationships As Source
- -| Contact Name | -- | - |
|---|---|---|
| {{ relationship.target }} | -- Profile - | -- Relationship Detail - | -
| No known relationships | -||
Relationships As Target
- -| Contact Name | -- | - |
|---|---|---|
| {{ relationship.source }} | -- Profile - | -- Relationship Detail - | -
| No known relationships | -||
- -
Activities
- -| Name | -|
|---|---|
| {{ activity }} | -- Details - | -
| No records | -
{{ person.name }}
+ ++ + {% if person.user != request.user and request.user.is_superuser %} +
+ + + +
+ + {% include 'people/person/includes/relationships_full.html' %} + +
+ + {% include 'people/person/includes/activities_full.html' %} + +
+ +{% endblock %} diff --git a/people/templates/people/person/detail_partial.html b/people/templates/people/person/detail_partial.html new file mode 100644 index 0000000..124213d --- /dev/null +++ b/people/templates/people/person/detail_partial.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block extra_head %} + {{ map_markers|json_script:'map-markers' }} + + {% load staticfiles %} + + + +{% endblock %} + +{% block content %} + + +
{{ person.name }}
+ + New Relationship + + ++ + {% include 'people/person/includes/answer_set_partial.html' %} + +
+ + + +
+{% endblock %} diff --git a/people/templates/people/person/includes/activities_full.html b/people/templates/people/person/includes/activities_full.html new file mode 100644 index 0000000..0f2b811 --- /dev/null +++ b/people/templates/people/person/includes/activities_full.html @@ -0,0 +1,26 @@ +
Activities
+ +| Name | +|
|---|---|
| {{ activity }} | ++ Details + | +
| No records | +
| Question | +Answer | +
|---|---|
| Country of Residence | {{ answer_set.country_of_residence.name }} |
| Organisation | {{ answer_set.organisation }} |
| {{ answer.question }} | +{{ answer }} | +
| No records | +
Last updated: {{ answer_set.timestamp }}
diff --git a/people/templates/people/person/includes/relationships_full.html b/people/templates/people/person/includes/relationships_full.html new file mode 100644 index 0000000..6f5350d --- /dev/null +++ b/people/templates/people/person/includes/relationships_full.html @@ -0,0 +1,36 @@ +People I've Answered Questions About
+ +| Contact Name | ++ |
|---|---|
| {{ relationship.target }} | ++ Profile + Relationship Detail + Update + | +
| No known relationships | +|