diff --git a/.gitignore b/.gitignore
index 848c6b0..206fcfe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,8 +15,7 @@ debug.log*
# Configuration
settings.ini
-deployment-key
-deployment-key.pub
+deployment-key*
# Deployment
/.dbbackup/
diff --git a/breccia_mapper/settings.py b/breccia_mapper/settings.py
index 16d6569..29b27d3 100644
--- a/breccia_mapper/settings.py
+++ b/breccia_mapper/settings.py
@@ -14,6 +14,8 @@ https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
"""
import collections
+import logging
+import logging.config
import pathlib
from django.urls import reverse_lazy
@@ -241,6 +243,12 @@ LOGGING = {
}
}
+# Initialise logger now so we can use it in this file
+
+LOGGING_CONFIG = None
+logging.config.dictConfig(LOGGING)
+logger = logging.getLogger(__name__)
+
# Admin panel variables
@@ -262,3 +270,20 @@ CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
BOOTSTRAP4 = {
'include_jquery': 'full',
}
+
+
+# Import customisation app settings if present
+
+TEMPLATE_NAME_INDEX = 'index.html'
+
+try:
+ from custom.settings import (
+ CUSTOMISATION_NAME,
+ TEMPLATE_NAME_INDEX
+ )
+ logger.info("Loaded customisation app: %s", CUSTOMISATION_NAME)
+
+ INSTALLED_APPS.append('custom')
+
+except ImportError as e:
+ logger.info("No customisation app loaded: %s", e)
diff --git a/breccia_mapper/static/css/masthead.css b/breccia_mapper/static/css/masthead.css
index 56fb41e..79b9aa6 100644
--- a/breccia_mapper/static/css/masthead.css
+++ b/breccia_mapper/static/css/masthead.css
@@ -1,14 +1,14 @@
header.masthead {
position: relative;
background: #343a40 no-repeat center;
- -webkit-background-size: cover;
- -moz-background-size: cover;
- -o-background-size: cover;
- background-size: cover;
+ -webkit-background-size: contain;
+ -moz-background-size: contain;
+ -o-background-size: contain;
+ background-size: contain;
padding-top: 8rem;
padding-bottom: 8rem;
- min-height: 200px;
- height: 60vh;
+ min-height: 400px;
+ height: 40vh;
z-index: -2;
}
diff --git a/breccia_mapper/views.py b/breccia_mapper/views.py
index 272f7c9..ff539f6 100644
--- a/breccia_mapper/views.py
+++ b/breccia_mapper/views.py
@@ -4,8 +4,10 @@ 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.views.generic import TemplateView
class IndexView(TemplateView):
- template_name = 'index.html'
+ # Template set in Django settings file - may be customised by a customisation app
+ template_name = settings.TEMPLATE_NAME_INDEX
diff --git a/export/serializers/people.py b/export/serializers/people.py
index 8c84ccb..196f295 100644
--- a/export/serializers/people.py
+++ b/export/serializers/people.py
@@ -11,7 +11,7 @@ class SimplePersonSerializer(serializers.ModelSerializer):
class Meta:
model = models.Person
fields = [
- 'pk',
+ 'id',
'name',
]
@@ -20,7 +20,7 @@ class PersonSerializer(base.FlattenedModelSerializer):
class Meta:
model = models.Person
fields = [
- 'pk',
+ 'id',
'name',
'core_member',
'gender',
@@ -28,8 +28,8 @@ class PersonSerializer(base.FlattenedModelSerializer):
'nationality',
'country_of_residence',
]
-
-
+
+
class RelationshipSerializer(base.FlattenedModelSerializer):
source = SimplePersonSerializer()
target = SimplePersonSerializer()
@@ -37,11 +37,24 @@ class RelationshipSerializer(base.FlattenedModelSerializer):
class Meta:
model = models.Relationship
fields = [
- 'pk',
+ 'id',
'source',
'target',
]
+
+class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer):
+ relationship = RelationshipSerializer()
+
+ class Meta:
+ model = models.RelationshipAnswerSet
+ fields = [
+ 'id',
+ 'relationship',
+ 'timestamp',
+ 'replaced_timestamp',
+ ]
+
@property
def column_headers(self) -> typing.List[str]:
headers = super().column_headers
@@ -57,7 +70,7 @@ class RelationshipSerializer(base.FlattenedModelSerializer):
try:
# Add relationship question answers to data
- for answer in instance.current_answers.question_answers.all():
+ for answer in instance.question_answers.all():
rep[answer.question.slug.replace('-', '_')] = answer.slug.replace('-', '_')
except AttributeError:
diff --git a/export/templates/export/export.html b/export/templates/export/export.html
index c94b2df..28a04f6 100644
--- a/export/templates/export/export.html
+++ b/export/templates/export/export.html
@@ -39,6 +39,15 @@
+
+ | Relationship Answer Sets |
+ |
+
+ Export
+ |
+
+
| Activities |
|
diff --git a/export/urls.py b/export/urls.py
index c56fca5..8fed824 100644
--- a/export/urls.py
+++ b/export/urls.py
@@ -17,6 +17,10 @@ urlpatterns = [
path('export/relationships',
views.people.RelationshipExportView.as_view(),
name='relationship'),
+
+ path('export/relationship-answer-sets',
+ views.people.RelationshipAnswerSetExportView.as_view(),
+ name='relationship-answer-set'),
path('export/activities',
views.activities.ActivityExportView.as_view(),
diff --git a/export/views/people.py b/export/views/people.py
index 7f73d5f..a9ff4a6 100644
--- a/export/views/people.py
+++ b/export/views/people.py
@@ -12,3 +12,8 @@ class PersonExportView(base.CsvExportView):
class RelationshipExportView(base.CsvExportView):
model = models.relationship.Relationship
serializer_class = serializers.people.RelationshipSerializer
+
+
+class RelationshipAnswerSetExportView(base.CsvExportView):
+ model = models.relationship.RelationshipAnswerSet
+ serializer_class = serializers.people.RelationshipAnswerSetSerializer
diff --git a/people/forms.py b/people/forms.py
index d230fbb..5877ce5 100644
--- a/people/forms.py
+++ b/people/forms.py
@@ -35,7 +35,7 @@ class PersonForm(forms.ModelForm):
class DynamicAnswerSetBase(forms.Form):
- field_class = forms.ChoiceField
+ field_class = forms.ModelChoiceField
field_widget = None
field_required = True
@@ -43,11 +43,8 @@ class DynamicAnswerSetBase(forms.Form):
super().__init__(*args, **kwargs)
for question in models.RelationshipQuestion.objects.all():
- # Get choices from model and add default 'not selected' option
- choices = question.choices + [['', '---------']]
-
field = self.field_class(label=question,
- choices=choices,
+ queryset=question.answers,
widget=self.field_widget,
required=self.field_required)
self.fields['question_{}'.format(question.pk)] = field
@@ -72,11 +69,8 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
if commit:
# Save answers to relationship questions
for key, value in self.cleaned_data.items():
- if key.startswith('question_'):
- question_id = key.replace('question_', '', 1)
- answer = models.RelationshipQuestionChoice.objects.get(pk=value,
- question__pk=question_id)
- self.instance.question_answers.add(answer)
+ if key.startswith('question_') and value:
+ self.instance.question_answers.add(value)
return self.instance
@@ -85,6 +79,12 @@ class NetworkFilterForm(DynamicAnswerSetBase):
"""
Form to provide filtering on the network view.
"""
- field_class = forms.MultipleChoiceField
+ field_class = forms.ModelMultipleChoiceField
field_widget = Select2MultipleWidget
field_required = False
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Add date field to select relationships at a particular point in time
+ self.fields['date'] = forms.DateField(required=False)
diff --git a/people/templates/people/network.html b/people/templates/people/network.html
index 144a27a..0901d13 100644
--- a/people/templates/people/network.html
+++ b/people/templates/people/network.html
@@ -18,8 +18,12 @@
Filter Relationships
- {% load bootstrap4 %}
- {% bootstrap_form form %}
+ {% load bootstrap4 %}
+ {% bootstrap_form form exclude='date' %}
+
+
+ {% bootstrap_field form.date %}
+
@@ -27,12 +31,14 @@
+
{% buttons %}
{% endbuttons %}
@@ -104,8 +110,8 @@
group: 'edges',
data: {
id: 'relationship-' + relationship.pk.toString(),
- source: 'person-' + relationship.source.toString(),
- target: 'person-' + relationship.target.toString()
+ source: 'person-' + relationship.source.pk.toString(),
+ target: 'person-' + relationship.target.pk.toString()
}
})
}
diff --git a/people/views/network.py b/people/views/network.py
index 3e02efa..a17ab9e 100644
--- a/people/views/network.py
+++ b/people/views/network.py
@@ -4,6 +4,7 @@ Views for displaying networks of :class:`People` and :class:`Relationship`s.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
+from django.forms import ValidationError
from django.utils import timezone
from django.views.generic import FormView
@@ -38,22 +39,24 @@ class NetworkView(LoginRequiredMixin, FormView):
Add filtered QuerySets of :class:`Person` and :class:`Relationship` to the context.
"""
context = super().get_context_data(**kwargs)
- form = context['form']
+ form: forms.NetworkFilterForm = context['form']
+ if not form.is_valid():
+ return context
- at_time = timezone.now()
+ at_date = form.cleaned_data['date']
+ if not at_date:
+ at_date = timezone.now().date()
- relationship_set = models.Relationship.objects.all()
+ relationship_answerset_set = models.RelationshipAnswerSet.objects.filter(
+ Q(replaced_timestamp__date__gte=at_date) | Q(replaced_timestamp__isnull=True),
+ timestamp__date__lte=at_date
+ )
# Filter answers to relationship questions
- for key, value in form.data.items():
- if key.startswith('question_') and value:
- question_id = key.replace('question_', '', 1)
- answer = models.RelationshipQuestionChoice.objects.get(pk=value,
- question__pk=question_id)
- relationship_set = relationship_set.filter(
- Q(answer_sets__replaced_timestamp__gt=at_time) | Q(answer_sets__replaced_timestamp__isnull=True),
- answer_sets__timestamp__lte=at_time,
- answer_sets__question_answers=answer
+ for field, values in form.cleaned_data.items():
+ if field.startswith('question_') and values:
+ relationship_answerset_set = relationship_answerset_set.filter(
+ question_answers__in=values
)
context['person_set'] = serializers.PersonSerializer(
@@ -62,11 +65,17 @@ class NetworkView(LoginRequiredMixin, FormView):
).data
context['relationship_set'] = serializers.RelationshipSerializer(
- relationship_set,
+ models.Relationship.objects.filter(
+ pk__in=relationship_answerset_set.values_list('relationship', flat=True)
+ ),
many=True
).data
return context
def form_valid(self, form):
- return self.render_to_response(self.get_context_data())
+ try:
+ return self.render_to_response(self.get_context_data())
+
+ except ValidationError:
+ return self.form_invalid(form)
diff --git a/people/views/relationship.py b/people/views/relationship.py
index eeb2ea6..8709b99 100644
--- a/people/views/relationship.py
+++ b/people/views/relationship.py
@@ -118,13 +118,12 @@ class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
"""
Mark any previous answer sets as replaced.
"""
- previous_valid_answer_sets = self.relationship.answer_sets.filter(replaced_timestamp__isnull=True)
-
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 previous_valid_answer_sets:
- answer_set.replaced_timestamp = timezone.now()
+ for answer_set in self.relationship.answer_sets.exclude(pk=self.object.pk):
+ answer_set.replaced_timestamp = now_date
answer_set.save()
return response
diff --git a/roles/webserver/tasks/main.yml b/roles/webserver/tasks/main.yml
index ef16b98..04103ab 100644
--- a/roles/webserver/tasks/main.yml
+++ b/roles/webserver/tasks/main.yml
@@ -63,20 +63,36 @@
- name: Copy deploy key
copy:
- src: 'deployment-key'
+ src: '{{ deployment_keyfile }}'
dest: '/tmp/deployment-key'
mode: 0600
- when: vagrant_dir.stat.exists == False
+ when: vagrant_dir.stat.exists == False and deployment_keyfile is defined
- name: Clone / update from source repo
git:
repo: 'git@github.com:Southampton-RSG/breccia-mapper.git'
dest: '{{ project_dir }}'
- key_file: '/tmp/deployment-key'
+ key_file: '{{ "/tmp/deployment-key" if deployment_keyfile is defined else None }}'
version: '{{ branch | default ("master") }}'
accept_hostkey: yes
when: vagrant_dir.stat.exists == False
+- name: Copy customisation deploy key
+ copy:
+ src: '{{ customisation_repo_keyfile }}'
+ dest: '/tmp/deployment-key-customisation'
+ mode: 0600
+ when: customisation_repo_keyfile is defined
+
+- name: Clone / update from customisation repo
+ git:
+ repo: '{{ customisation_repo }}'
+ dest: '{{ project_dir }}/custom'
+ key_file: '{{ "/tmp/deployment-key-customisation" if customisation_repo_keyfile is defined else None }}'
+ version: '{{ branch | default ("master") }}'
+ accept_hostkey: yes
+ when: customisation_repo is defined
+
- name: Copy and populate settings template
template:
src: 'settings.j2'