Merge branch 'dev'

This commit is contained in:
James Graham
2020-04-21 09:39:55 +01:00
13 changed files with 138 additions and 51 deletions

3
.gitignore vendored
View File

@@ -15,8 +15,7 @@ debug.log*
# Configuration # Configuration
settings.ini settings.ini
deployment-key deployment-key*
deployment-key.pub
# Deployment # Deployment
/.dbbackup/ /.dbbackup/

View File

@@ -14,6 +14,8 @@ https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
""" """
import collections import collections
import logging
import logging.config
import pathlib import pathlib
from django.urls import reverse_lazy 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 # Admin panel variables
@@ -262,3 +270,20 @@ CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
BOOTSTRAP4 = { BOOTSTRAP4 = {
'include_jquery': 'full', '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)

View File

@@ -1,14 +1,14 @@
header.masthead { header.masthead {
position: relative; position: relative;
background: #343a40 no-repeat center; background: #343a40 no-repeat center;
-webkit-background-size: cover; -webkit-background-size: contain;
-moz-background-size: cover; -moz-background-size: contain;
-o-background-size: cover; -o-background-size: contain;
background-size: cover; background-size: contain;
padding-top: 8rem; padding-top: 8rem;
padding-bottom: 8rem; padding-bottom: 8rem;
min-height: 200px; min-height: 400px;
height: 60vh; height: 40vh;
z-index: -2; z-index: -2;
} }

View File

@@ -4,8 +4,10 @@ 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.views.generic import TemplateView from django.views.generic import TemplateView
class IndexView(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

View File

@@ -11,7 +11,7 @@ class SimplePersonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Person model = models.Person
fields = [ fields = [
'pk', 'id',
'name', 'name',
] ]
@@ -20,7 +20,7 @@ class PersonSerializer(base.FlattenedModelSerializer):
class Meta: class Meta:
model = models.Person model = models.Person
fields = [ fields = [
'pk', 'id',
'name', 'name',
'core_member', 'core_member',
'gender', 'gender',
@@ -37,11 +37,24 @@ class RelationshipSerializer(base.FlattenedModelSerializer):
class Meta: class Meta:
model = models.Relationship model = models.Relationship
fields = [ fields = [
'pk', 'id',
'source', 'source',
'target', 'target',
] ]
class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer):
relationship = RelationshipSerializer()
class Meta:
model = models.RelationshipAnswerSet
fields = [
'id',
'relationship',
'timestamp',
'replaced_timestamp',
]
@property @property
def column_headers(self) -> typing.List[str]: def column_headers(self) -> typing.List[str]:
headers = super().column_headers headers = super().column_headers
@@ -57,7 +70,7 @@ class RelationshipSerializer(base.FlattenedModelSerializer):
try: try:
# Add relationship question answers to data # 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('-', '_') rep[answer.question.slug.replace('-', '_')] = answer.slug.replace('-', '_')
except AttributeError: except AttributeError:

View File

@@ -39,6 +39,15 @@
</td> </td>
</tr> </tr>
<tr>
<td>Relationship Answer Sets</td>
<td></td>
<td>
<a class="btn btn-info"
href="{% url 'export:relationship-answer-set' %}">Export</a>
</td>
</tr>
<tr> <tr>
<td>Activities</td> <td>Activities</td>
<td></td> <td></td>

View File

@@ -18,6 +18,10 @@ urlpatterns = [
views.people.RelationshipExportView.as_view(), views.people.RelationshipExportView.as_view(),
name='relationship'), name='relationship'),
path('export/relationship-answer-sets',
views.people.RelationshipAnswerSetExportView.as_view(),
name='relationship-answer-set'),
path('export/activities', path('export/activities',
views.activities.ActivityExportView.as_view(), views.activities.ActivityExportView.as_view(),
name='activity'), name='activity'),

View File

@@ -12,3 +12,8 @@ class PersonExportView(base.CsvExportView):
class RelationshipExportView(base.CsvExportView): class RelationshipExportView(base.CsvExportView):
model = models.relationship.Relationship model = models.relationship.Relationship
serializer_class = serializers.people.RelationshipSerializer serializer_class = serializers.people.RelationshipSerializer
class RelationshipAnswerSetExportView(base.CsvExportView):
model = models.relationship.RelationshipAnswerSet
serializer_class = serializers.people.RelationshipAnswerSetSerializer

View File

@@ -35,7 +35,7 @@ class PersonForm(forms.ModelForm):
class DynamicAnswerSetBase(forms.Form): class DynamicAnswerSetBase(forms.Form):
field_class = forms.ChoiceField field_class = forms.ModelChoiceField
field_widget = None field_widget = None
field_required = True field_required = True
@@ -43,11 +43,8 @@ class DynamicAnswerSetBase(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for question in models.RelationshipQuestion.objects.all(): 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, field = self.field_class(label=question,
choices=choices, queryset=question.answers,
widget=self.field_widget, widget=self.field_widget,
required=self.field_required) required=self.field_required)
self.fields['question_{}'.format(question.pk)] = field self.fields['question_{}'.format(question.pk)] = field
@@ -72,11 +69,8 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
if commit: if commit:
# 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_'): if key.startswith('question_') and value:
question_id = key.replace('question_', '', 1) self.instance.question_answers.add(value)
answer = models.RelationshipQuestionChoice.objects.get(pk=value,
question__pk=question_id)
self.instance.question_answers.add(answer)
return self.instance return self.instance
@@ -85,6 +79,12 @@ class NetworkFilterForm(DynamicAnswerSetBase):
""" """
Form to provide filtering on the network view. Form to provide filtering on the network view.
""" """
field_class = forms.MultipleChoiceField field_class = forms.ModelMultipleChoiceField
field_widget = Select2MultipleWidget field_widget = Select2MultipleWidget
field_required = False 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)

View File

@@ -19,7 +19,11 @@
<div class="col-md-6"> <div class="col-md-6">
<h3>Filter Relationships</h3> <h3>Filter Relationships</h3>
{% load bootstrap4 %} {% load bootstrap4 %}
{% bootstrap_form form %} {% bootstrap_form form exclude='date' %}
<hr>
{% bootstrap_field form.date %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@@ -27,12 +31,14 @@
</div> </div>
</div> </div>
{% buttons %} {% buttons %}
<button class="btn btn-block btn-info" type="submit">Filter</button> <button class="btn btn-block btn-info" type="submit">Filter</button>
{% endbuttons %} {% endbuttons %}
</form> </form>
<div id="cy" <div id="cy"
class="mb-2"
style="width: 100%; min-height: 1000px; flex-grow: 1; border: 2px solid black"></div> style="width: 100%; min-height: 1000px; flex-grow: 1; border: 2px solid black"></div>
@@ -104,8 +110,8 @@
group: 'edges', group: 'edges',
data: { data: {
id: 'relationship-' + relationship.pk.toString(), id: 'relationship-' + relationship.pk.toString(),
source: 'person-' + relationship.source.toString(), source: 'person-' + relationship.source.pk.toString(),
target: 'person-' + relationship.target.toString() target: 'person-' + relationship.target.pk.toString()
} }
}) })
} }

View File

@@ -4,6 +4,7 @@ Views for displaying networks of :class:`People` and :class:`Relationship`s.
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from django.forms import ValidationError
from django.utils import timezone from django.utils import timezone
from django.views.generic import FormView 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. Add filtered QuerySets of :class:`Person` and :class:`Relationship` to the context.
""" """
context = super().get_context_data(**kwargs) 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 # Filter answers to relationship questions
for key, value in form.data.items(): for field, values in form.cleaned_data.items():
if key.startswith('question_') and value: if field.startswith('question_') and values:
question_id = key.replace('question_', '', 1) relationship_answerset_set = relationship_answerset_set.filter(
answer = models.RelationshipQuestionChoice.objects.get(pk=value, question_answers__in=values
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
) )
context['person_set'] = serializers.PersonSerializer( context['person_set'] = serializers.PersonSerializer(
@@ -62,11 +65,17 @@ class NetworkView(LoginRequiredMixin, FormView):
).data ).data
context['relationship_set'] = serializers.RelationshipSerializer( context['relationship_set'] = serializers.RelationshipSerializer(
relationship_set, models.Relationship.objects.filter(
pk__in=relationship_answerset_set.values_list('relationship', flat=True)
),
many=True many=True
).data ).data
return context return context
def form_valid(self, form): def form_valid(self, form):
try:
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
except ValidationError:
return self.form_invalid(form)

View File

@@ -118,13 +118,12 @@ class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
""" """
Mark any previous answer sets as replaced. 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) response = super().form_valid(form)
now_date = timezone.now().date()
# Shouldn't be more than one after initial updates after migration # Shouldn't be more than one after initial updates after migration
for answer_set in previous_valid_answer_sets: for answer_set in self.relationship.answer_sets.exclude(pk=self.object.pk):
answer_set.replaced_timestamp = timezone.now() answer_set.replaced_timestamp = now_date
answer_set.save() answer_set.save()
return response return response

View File

@@ -63,20 +63,36 @@
- name: Copy deploy key - name: Copy deploy key
copy: copy:
src: 'deployment-key' src: '{{ deployment_keyfile }}'
dest: '/tmp/deployment-key' dest: '/tmp/deployment-key'
mode: 0600 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 - name: Clone / update from source repo
git: git:
repo: 'git@github.com:Southampton-RSG/breccia-mapper.git' repo: 'git@github.com:Southampton-RSG/breccia-mapper.git'
dest: '{{ project_dir }}' dest: '{{ project_dir }}'
key_file: '/tmp/deployment-key' key_file: '{{ "/tmp/deployment-key" if deployment_keyfile is defined else None }}'
version: '{{ branch | default ("master") }}' version: '{{ branch | default ("master") }}'
accept_hostkey: yes accept_hostkey: yes
when: vagrant_dir.stat.exists == False 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 - name: Copy and populate settings template
template: template:
src: 'settings.j2' src: 'settings.j2'