mirror of
https://github.com/Southampton-RSG/breccia-mapper.git
synced 2026-03-03 11:27:09 +00:00
Merge branch 'dev'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,8 +15,7 @@ debug.log*
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
settings.ini
|
settings.ini
|
||||||
deployment-key
|
deployment-key*
|
||||||
deployment-key.pub
|
|
||||||
|
|
||||||
# Deployment
|
# Deployment
|
||||||
/.dbbackup/
|
/.dbbackup/
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -28,8 +28,8 @@ class PersonSerializer(base.FlattenedModelSerializer):
|
|||||||
'nationality',
|
'nationality',
|
||||||
'country_of_residence',
|
'country_of_residence',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RelationshipSerializer(base.FlattenedModelSerializer):
|
class RelationshipSerializer(base.FlattenedModelSerializer):
|
||||||
source = SimplePersonSerializer()
|
source = SimplePersonSerializer()
|
||||||
target = SimplePersonSerializer()
|
target = SimplePersonSerializer()
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ urlpatterns = [
|
|||||||
path('export/relationships',
|
path('export/relationships',
|
||||||
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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -18,8 +18,12 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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):
|
||||||
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user