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
@@ -16,6 +16,7 @@ deployment-key
|
|||||||
deployment-key.pub
|
deployment-key.pub
|
||||||
|
|
||||||
# Deployment
|
# Deployment
|
||||||
|
/.dbbackup/
|
||||||
.vagrant/
|
.vagrant/
|
||||||
staging.yml
|
staging.yml
|
||||||
/.dbbackup/
|
production.yml
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -9,8 +9,8 @@ lint:
|
|||||||
|
|
||||||
.PHONY: staging
|
.PHONY: staging
|
||||||
staging:
|
staging:
|
||||||
ansible-playbook -v -i staging.yml playbook.yml -u jag1e17 -K
|
env ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i staging.yml playbook.yml -u jag1e17 -K
|
||||||
|
|
||||||
.PHONY: production
|
.PHONY: production
|
||||||
production:
|
production:
|
||||||
ansible-playbook -v -i production.yml playbook.yml -u jag1e17 -K
|
env ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i production.yml playbook.yml -u jag1e17 -K
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Views for displaying / manipulating models within the Activities app.
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.views.generic import DetailView, ListView, View
|
from django.views.generic import DetailView, ListView, View
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
@@ -12,7 +13,7 @@ from people import permissions
|
|||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class ActivitySeriesListView(ListView):
|
class ActivitySeriesListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
View displaying a list of :class:`ActivitySeries`.
|
View displaying a list of :class:`ActivitySeries`.
|
||||||
"""
|
"""
|
||||||
@@ -21,7 +22,7 @@ class ActivitySeriesListView(ListView):
|
|||||||
context_object_name = 'activity_series_list'
|
context_object_name = 'activity_series_list'
|
||||||
|
|
||||||
|
|
||||||
class ActivitySeriesDetailView(DetailView):
|
class ActivitySeriesDetailView(LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View displaying details of a single :class:`ActivitySeries`.
|
View displaying details of a single :class:`ActivitySeries`.
|
||||||
"""
|
"""
|
||||||
@@ -30,7 +31,7 @@ class ActivitySeriesDetailView(DetailView):
|
|||||||
context_object_name = 'activity_series'
|
context_object_name = 'activity_series'
|
||||||
|
|
||||||
|
|
||||||
class ActivityListView(ListView):
|
class ActivityListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
View displaying a list of :class:`Activity`.
|
View displaying a list of :class:`Activity`.
|
||||||
"""
|
"""
|
||||||
@@ -38,7 +39,7 @@ class ActivityListView(ListView):
|
|||||||
template_name = 'activities/activity/list.html'
|
template_name = 'activities/activity/list.html'
|
||||||
|
|
||||||
|
|
||||||
class ActivityDetailView(DetailView):
|
class ActivityDetailView(LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View displaying details of a single :class:`Activity`.
|
View displaying details of a single :class:`Activity`.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ THIRD_PARTY_APPS = [
|
|||||||
FIRST_PARTY_APPS = [
|
FIRST_PARTY_APPS = [
|
||||||
'people',
|
'people',
|
||||||
'activities',
|
'activities',
|
||||||
|
'export',
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + FIRST_PARTY_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + FIRST_PARTY_APPS
|
||||||
@@ -141,6 +142,11 @@ REST_FRAMEWORK = {
|
|||||||
'DEFAULT_PERMISSION_CLASSES': [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
],
|
],
|
||||||
|
'DEFAULT_RENDERER_CLASSES': [
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
'rest_framework_csv.renderers.CSVRenderer',
|
||||||
|
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,10 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if request.user.is_superuser %}
|
{% if request.user.is_superuser %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'export:index' %}" class="nav-link">Export</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{% url 'admin:index' %}" class="nav-link">Admin</a>
|
<a href="{% url 'admin:index' %}" class="nav-link">Admin</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ urlpatterns = [
|
|||||||
views.IndexView.as_view(),
|
views.IndexView.as_view(),
|
||||||
name='index'),
|
name='index'),
|
||||||
|
|
||||||
|
path('',
|
||||||
|
include('export.urls')),
|
||||||
|
|
||||||
path('',
|
path('',
|
||||||
include('people.urls')),
|
include('people.urls')),
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Views belonging to the core of the project.
|
||||||
|
|
||||||
|
These views don't represent any of the models in the apps.
|
||||||
|
"""
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
export/__init__.py
Normal file
0
export/__init__.py
Normal file
5
export/apps.py
Normal file
5
export/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ExportConfig(AppConfig):
|
||||||
|
name = 'export'
|
||||||
4
export/serializers/__init__.py
Normal file
4
export/serializers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from . import (
|
||||||
|
activities,
|
||||||
|
people
|
||||||
|
)
|
||||||
52
export/serializers/activities.py
Normal file
52
export/serializers/activities.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from activities import models
|
||||||
|
|
||||||
|
from . import base
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityTypeSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.ActivityType
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityMediumSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.ActivityMedium
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ActivitySeriesSerializer(serializers.ModelSerializer):
|
||||||
|
type = ActivityTypeSerializer()
|
||||||
|
medium = ActivityMediumSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.ActivitySeries
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'medium',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ActivitySerializer(base.FlattenedModelSerializer):
|
||||||
|
series = ActivitySeriesSerializer()
|
||||||
|
type = ActivityTypeSerializer()
|
||||||
|
medium = ActivityMediumSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Activity
|
||||||
|
fields = [
|
||||||
|
'name',
|
||||||
|
'series',
|
||||||
|
'type',
|
||||||
|
'medium',
|
||||||
|
]
|
||||||
65
export/serializers/base.py
Normal file
65
export/serializers/base.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Serialize models to and deserialize from JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class FlattenedModelSerializer(serializers.ModelSerializer):
|
||||||
|
@classmethod
|
||||||
|
def flatten_data(cls, data,
|
||||||
|
sub_type: typing.Type = dict,
|
||||||
|
sub_value_accessor: typing.Callable = lambda x: x.items()) -> OrderedDict:
|
||||||
|
"""
|
||||||
|
Flatten a dictionary so that subdictionaries become a series of `key[.subkey[.subsubkey ...]]` entries
|
||||||
|
in the top level dictionary.
|
||||||
|
|
||||||
|
Works for other data structures (e.g. DRF Serializers) by providing suitable values for the
|
||||||
|
`sub_type` and `sub_value_accessor` parameters.
|
||||||
|
|
||||||
|
:param data: Dictionary or other data structure to flatten
|
||||||
|
:param sub_type: Type to recursively flatten
|
||||||
|
:param sub_value_accessor: Function to access keys and values contained within sub_type.
|
||||||
|
"""
|
||||||
|
data_out = OrderedDict()
|
||||||
|
|
||||||
|
for key, value in sub_value_accessor(data):
|
||||||
|
if isinstance(value, sub_type):
|
||||||
|
# Recursively flatten nested structures of type `sub_type`
|
||||||
|
sub_flattened = cls.flatten_data(value,
|
||||||
|
sub_type=sub_type,
|
||||||
|
sub_value_accessor=sub_value_accessor).items()
|
||||||
|
|
||||||
|
# Enter recursively flattened values into result dictionary
|
||||||
|
for sub_key, sub_value in sub_flattened:
|
||||||
|
# Keys in result dictionary are of format `key[.subkey[.subsubkey ...]]`
|
||||||
|
data_out[f'{key}.{sub_key}'] = sub_value
|
||||||
|
|
||||||
|
elif value is not None:
|
||||||
|
data_out[key] = value
|
||||||
|
|
||||||
|
return data_out
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column_headers(self) -> typing.List[str]:
|
||||||
|
"""
|
||||||
|
Get all column headers that will be output by this serializer.
|
||||||
|
"""
|
||||||
|
fields = self.flatten_data(self.fields,
|
||||||
|
sub_type=serializers.BaseSerializer,
|
||||||
|
sub_value_accessor=lambda x: x.fields.items())
|
||||||
|
|
||||||
|
return list(fields)
|
||||||
|
|
||||||
|
def to_representation(self, instance) -> OrderedDict:
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
rep = super().to_representation(instance)
|
||||||
|
|
||||||
|
rep = self.flatten_data(rep)
|
||||||
|
|
||||||
|
return rep
|
||||||
66
export/serializers/people.py
Normal file
66
export/serializers/people.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from people import models
|
||||||
|
|
||||||
|
from . import base
|
||||||
|
|
||||||
|
|
||||||
|
class SimplePersonSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.Person
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PersonSerializer(base.FlattenedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.Person
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
'core_member',
|
||||||
|
'gender',
|
||||||
|
'age_group',
|
||||||
|
'nationality',
|
||||||
|
'country_of_residence',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RelationshipSerializer(base.FlattenedModelSerializer):
|
||||||
|
source = SimplePersonSerializer()
|
||||||
|
target = SimplePersonSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Relationship
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'source',
|
||||||
|
'target',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column_headers(self) -> typing.List[str]:
|
||||||
|
headers = super().column_headers
|
||||||
|
|
||||||
|
# Add relationship questions to columns
|
||||||
|
for question in models.RelationshipQuestion.objects.all():
|
||||||
|
headers.append(question.slug)
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
rep = super().to_representation(instance)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Add relationship question answers to data
|
||||||
|
for answer in instance.current_answers.question_answers.all():
|
||||||
|
rep[answer.question.slug] = answer.slug
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return rep
|
||||||
52
export/templates/export/export.html
Normal file
52
export/templates/export/export.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Export Data</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Records</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>People</td>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-info"
|
||||||
|
href="{% url 'export:person' %}">Export</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Relationships</td>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-info"
|
||||||
|
href="{% url 'export:relationship' %}">Export</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Activities</td>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-info"
|
||||||
|
href="{% url 'export:activity' %}">Export</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
24
export/urls.py
Normal file
24
export/urls.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
app_name = 'export'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('export',
|
||||||
|
views.ExportListView.as_view(),
|
||||||
|
name='index'),
|
||||||
|
|
||||||
|
path('export/people',
|
||||||
|
views.people.PersonExportView.as_view(),
|
||||||
|
name='person'),
|
||||||
|
|
||||||
|
path('export/relationships',
|
||||||
|
views.people.RelationshipExportView.as_view(),
|
||||||
|
name='relationship'),
|
||||||
|
|
||||||
|
path('export/activities',
|
||||||
|
views.activities.ActivityExportView.as_view(),
|
||||||
|
name='activity'),
|
||||||
|
]
|
||||||
6
export/views/__init__.py
Normal file
6
export/views/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .base import ExportListView
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
activities,
|
||||||
|
people
|
||||||
|
)
|
||||||
9
export/views/activities.py
Normal file
9
export/views/activities.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from . import base
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
from activities import models
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityExportView(base.CsvExportView):
|
||||||
|
model = models.Activity
|
||||||
|
serializer_class = serializers.activities.ActivitySerializer
|
||||||
29
export/views/base.py
Normal file
29
export/views/base.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import csv
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.views.generic.list import BaseListView
|
||||||
|
|
||||||
|
|
||||||
|
class CsvExportView(LoginRequiredMixin, BaseListView):
|
||||||
|
model = None
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
|
def render_to_response(self, context: typing.Dict) -> HttpResponse:
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{self.get_context_object_name(self.object_list)}.csv"'
|
||||||
|
|
||||||
|
# Force ordering by PK - though this should be default anyway
|
||||||
|
serializer = self.serializer_class(self.get_queryset().order_by('pk'), many=True)
|
||||||
|
|
||||||
|
writer = csv.DictWriter(response, fieldnames=serializer.child.column_headers)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(serializer.data)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportListView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'export/export.html'
|
||||||
14
export/views/people.py
Normal file
14
export/views/people.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from . import base
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
from people import models
|
||||||
|
|
||||||
|
|
||||||
|
class PersonExportView(base.CsvExportView):
|
||||||
|
model = models.person.Person
|
||||||
|
serializer_class = serializers.people.PersonSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class RelationshipExportView(base.CsvExportView):
|
||||||
|
model = models.relationship.Relationship
|
||||||
|
serializer_class = serializers.people.RelationshipSerializer
|
||||||
@@ -6,6 +6,7 @@ import typing
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from .person import Person
|
from .person import Person
|
||||||
|
|
||||||
@@ -42,12 +43,16 @@ class RelationshipQuestion(models.Model):
|
|||||||
@property
|
@property
|
||||||
def choices(self) -> typing.List[typing.List[str]]:
|
def choices(self) -> typing.List[typing.List[str]]:
|
||||||
"""
|
"""
|
||||||
Convert the :class:`RelationshipQuestionChoices` for this question into Django choices.
|
Convert the :class:`RelationshipQuestionChoice`s for this question into Django choices.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
[choice.pk, str(choice)] for choice in self.answers.all()
|
[choice.pk, str(choice)] for choice in self.answers.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def slug(self) -> str:
|
||||||
|
return slugify(self.text)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
@@ -80,6 +85,10 @@ class RelationshipQuestionChoice(models.Model):
|
|||||||
order = models.SmallIntegerField(default=0,
|
order = models.SmallIntegerField(default=0,
|
||||||
blank=False, null=False)
|
blank=False, null=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def slug(self) -> str:
|
||||||
|
return slugify(self.text)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class PersonSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class RelationshipSerializer(serializers.ModelSerializer):
|
class RelationshipSerializer(serializers.ModelSerializer):
|
||||||
|
source = PersonSerializer()
|
||||||
|
target = PersonSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Relationship
|
model = models.Relationship
|
||||||
fields = [
|
fields = [
|
||||||
|
|||||||
@@ -7,46 +7,38 @@ app_name = 'people'
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('profile/',
|
path('profile/',
|
||||||
views.ProfileView.as_view(),
|
views.person.ProfileView.as_view(),
|
||||||
name='person.profile'),
|
name='person.profile'),
|
||||||
|
|
||||||
path('people/create',
|
path('people/create',
|
||||||
views.PersonCreateView.as_view(),
|
views.person.PersonCreateView.as_view(),
|
||||||
name='person.create'),
|
name='person.create'),
|
||||||
|
|
||||||
path('people',
|
path('people',
|
||||||
views.PersonListView.as_view(),
|
views.person.PersonListView.as_view(),
|
||||||
name='person.list'),
|
name='person.list'),
|
||||||
|
|
||||||
path('people/<int:pk>',
|
path('people/<int:pk>',
|
||||||
views.ProfileView.as_view(),
|
views.person.ProfileView.as_view(),
|
||||||
name='person.detail'),
|
name='person.detail'),
|
||||||
|
|
||||||
path('people/<int:pk>/update',
|
path('people/<int:pk>/update',
|
||||||
views.PersonUpdateView.as_view(),
|
views.person.PersonUpdateView.as_view(),
|
||||||
name='person.update'),
|
name='person.update'),
|
||||||
|
|
||||||
path('people/<int:person_pk>/relationships/create',
|
path('people/<int:person_pk>/relationships/create',
|
||||||
views.RelationshipCreateView.as_view(),
|
views.relationship.RelationshipCreateView.as_view(),
|
||||||
name='person.relationship.create'),
|
name='person.relationship.create'),
|
||||||
|
|
||||||
path('relationships/<int:pk>',
|
path('relationships/<int:pk>',
|
||||||
views.RelationshipDetailView.as_view(),
|
views.relationship.RelationshipDetailView.as_view(),
|
||||||
name='relationship.detail'),
|
name='relationship.detail'),
|
||||||
|
|
||||||
path('relationships/<int:relationship_pk>/update',
|
path('relationships/<int:relationship_pk>/update',
|
||||||
views.RelationshipUpdateView.as_view(),
|
views.relationship.RelationshipUpdateView.as_view(),
|
||||||
name='relationship.update'),
|
name='relationship.update'),
|
||||||
|
|
||||||
path('api/people',
|
|
||||||
views.PersonApiView.as_view(),
|
|
||||||
name='person.api.list'),
|
|
||||||
|
|
||||||
path('api/relationships',
|
|
||||||
views.RelationshipApiView.as_view(),
|
|
||||||
name='relationship.api.list'),
|
|
||||||
|
|
||||||
path('network',
|
path('network',
|
||||||
views.NetworkView.as_view(),
|
views.network.NetworkView.as_view(),
|
||||||
name='network'),
|
name='network'),
|
||||||
]
|
]
|
||||||
|
|||||||
276
people/views.py
276
people/views.py
@@ -1,276 +0,0 @@
|
|||||||
"""
|
|
||||||
Views for displaying or manipulating models in the 'people' app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.views.generic import CreateView, DetailView, FormView, ListView, UpdateView
|
|
||||||
|
|
||||||
from rest_framework.views import APIView, Response
|
|
||||||
|
|
||||||
from . import forms, models, permissions, serializers
|
|
||||||
|
|
||||||
|
|
||||||
class PersonCreateView(CreateView):
|
|
||||||
"""
|
|
||||||
View to create a new instance of :class:`Person`.
|
|
||||||
|
|
||||||
If 'user' is passed as a URL parameter - link the new person to the current user.
|
|
||||||
"""
|
|
||||||
model = models.Person
|
|
||||||
template_name = 'people/person/create.html'
|
|
||||||
form_class = forms.PersonForm
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
if 'user' in self.request.GET:
|
|
||||||
form.instance.user = self.request.user
|
|
||||||
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class PersonListView(ListView):
|
|
||||||
"""
|
|
||||||
View displaying a list of :class:`Person` objects - searchable.
|
|
||||||
"""
|
|
||||||
model = models.Person
|
|
||||||
template_name = 'people/person/list.html'
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileView(permissions.UserIsLinkedPersonMixin, 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_object(self, queryset=None) -> models.Person:
|
|
||||||
"""
|
|
||||||
Get the :class:`Person` object to be represented by this page.
|
|
||||||
|
|
||||||
If not determined from url get current user.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return super().get_object(queryset)
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
# pk was not provided in URL
|
|
||||||
return self.request.user.person
|
|
||||||
|
|
||||||
|
|
||||||
class PersonUpdateView(permissions.UserIsLinkedPersonMixin, UpdateView):
|
|
||||||
"""
|
|
||||||
View for updating a :class:`Person` record.
|
|
||||||
"""
|
|
||||||
model = models.Person
|
|
||||||
template_name = 'people/person/update.html'
|
|
||||||
form_class = forms.PersonForm
|
|
||||||
|
|
||||||
|
|
||||||
class RelationshipDetailView(permissions.UserIsLinkedPersonMixin, DetailView):
|
|
||||||
"""
|
|
||||||
View displaying details of a :class:`Relationship`.
|
|
||||||
"""
|
|
||||||
model = models.Relationship
|
|
||||||
template_name = 'people/relationship/detail.html'
|
|
||||||
related_person_field = 'source'
|
|
||||||
|
|
||||||
|
|
||||||
class RelationshipCreateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
|
||||||
"""
|
|
||||||
View for creating a :class:`Relationship`.
|
|
||||||
|
|
||||||
Displays / processes a form containing the :class:`RelationshipQuestion`s.
|
|
||||||
"""
|
|
||||||
model = models.Relationship
|
|
||||||
template_name = 'people/relationship/create.html'
|
|
||||||
fields = [
|
|
||||||
'source',
|
|
||||||
'target',
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_test_person(self) -> models.Person:
|
|
||||||
"""
|
|
||||||
Get the person instance which should be used for access control checks.
|
|
||||||
"""
|
|
||||||
if self.request.method == 'POST':
|
|
||||||
return models.Person.objects.get(pk=self.request.POST.get('source'))
|
|
||||||
|
|
||||||
return models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
self.person = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
|
||||||
|
|
||||||
return super().get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
self.person = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
|
||||||
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
initial = super().get_initial()
|
|
||||||
|
|
||||||
initial['source'] = self.request.user.person
|
|
||||||
initial['target'] = self.person
|
|
||||||
|
|
||||||
return initial
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
context['person'] = self.person
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('people:relationship.update', kwargs={'relationship_pk': self.object.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
|
||||||
"""
|
|
||||||
View for creating a :class:`Relationship`.
|
|
||||||
|
|
||||||
Displays / processes a form containing the :class:`RelationshipQuestion`s.
|
|
||||||
"""
|
|
||||||
model = models.RelationshipAnswerSet
|
|
||||||
template_name = 'people/relationship/update.html'
|
|
||||||
form_class = forms.RelationshipAnswerSetForm
|
|
||||||
|
|
||||||
def get_test_person(self) -> models.Person:
|
|
||||||
"""
|
|
||||||
Get the person instance which should be used for access control checks.
|
|
||||||
"""
|
|
||||||
relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
|
||||||
|
|
||||||
return relationship.source
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
self.relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
|
||||||
self.person = self.relationship.source
|
|
||||||
|
|
||||||
return super().get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
self.relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
|
||||||
self.person = self.relationship.source
|
|
||||||
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
context['person'] = self.person
|
|
||||||
context['relationship'] = self.relationship
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
initial = super().get_initial()
|
|
||||||
|
|
||||||
initial['relationship'] = self.relationship
|
|
||||||
|
|
||||||
return initial
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
answer_set.save()
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class PersonApiView(APIView):
|
|
||||||
"""
|
|
||||||
List all :class:`Person` instances.
|
|
||||||
"""
|
|
||||||
def get(self, request, format=None):
|
|
||||||
"""
|
|
||||||
List all :class:`Person` instances.
|
|
||||||
"""
|
|
||||||
serializer = serializers.PersonSerializer(models.Person.objects.all(),
|
|
||||||
many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RelationshipApiView(APIView):
|
|
||||||
"""
|
|
||||||
List all :class:`Relationship` instances.
|
|
||||||
"""
|
|
||||||
def get(self, request, format=None):
|
|
||||||
"""
|
|
||||||
List all :class:`Relationship` instances.
|
|
||||||
"""
|
|
||||||
serializer = serializers.RelationshipSerializer(models.Relationship.objects.all(),
|
|
||||||
many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkView(LoginRequiredMixin, FormView):
|
|
||||||
"""
|
|
||||||
View to display relationship network.
|
|
||||||
"""
|
|
||||||
template_name = 'people/network.html'
|
|
||||||
form_class = forms.NetworkFilterForm
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
"""
|
|
||||||
Add GET params to form data.
|
|
||||||
"""
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
|
|
||||||
if self.request.method == 'GET':
|
|
||||||
if 'data' in kwargs:
|
|
||||||
kwargs['data'].update(self.request.GET)
|
|
||||||
|
|
||||||
else:
|
|
||||||
kwargs['data'] = self.request.GET
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Add filtered QuerySets of :class:`Person` and :class:`Relationship` to the context.
|
|
||||||
"""
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
form = context['form']
|
|
||||||
|
|
||||||
at_time = timezone.now()
|
|
||||||
|
|
||||||
relationship_set = models.Relationship.objects.all()
|
|
||||||
|
|
||||||
# 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
|
|
||||||
)
|
|
||||||
|
|
||||||
context['person_set'] = serializers.PersonSerializer(
|
|
||||||
models.Person.objects.all(),
|
|
||||||
many=True
|
|
||||||
).data
|
|
||||||
|
|
||||||
context['relationship_set'] = serializers.RelationshipSerializer(
|
|
||||||
relationship_set,
|
|
||||||
many=True
|
|
||||||
).data
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
return self.render_to_response(self.get_context_data())
|
|
||||||
9
people/views/__init__.py
Normal file
9
people/views/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Views for displaying or manipulating models within the `people` app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
network,
|
||||||
|
person,
|
||||||
|
relationship
|
||||||
|
)
|
||||||
72
people/views/network.py
Normal file
72
people/views/network.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
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.utils import timezone
|
||||||
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
|
||||||
|
from people import forms, models, serializers
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkView(LoginRequiredMixin, FormView):
|
||||||
|
"""
|
||||||
|
View to display relationship network.
|
||||||
|
"""
|
||||||
|
template_name = 'people/network.html'
|
||||||
|
form_class = forms.NetworkFilterForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
"""
|
||||||
|
Add GET params to form data.
|
||||||
|
"""
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
if 'data' in kwargs:
|
||||||
|
kwargs['data'].update(self.request.GET)
|
||||||
|
|
||||||
|
else:
|
||||||
|
kwargs['data'] = self.request.GET
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Add filtered QuerySets of :class:`Person` and :class:`Relationship` to the context.
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
form = context['form']
|
||||||
|
|
||||||
|
at_time = timezone.now()
|
||||||
|
|
||||||
|
relationship_set = models.Relationship.objects.all()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
|
||||||
|
context['person_set'] = serializers.PersonSerializer(
|
||||||
|
models.Person.objects.all(),
|
||||||
|
many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
context['relationship_set'] = serializers.RelationshipSerializer(
|
||||||
|
relationship_set,
|
||||||
|
many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
return self.render_to_response(self.get_context_data())
|
||||||
63
people/views/person.py
Normal file
63
people/views/person.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
Views for displaying or manipulating instances of :class:`Person`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.views.generic import CreateView, DetailView, ListView, UpdateView
|
||||||
|
|
||||||
|
from people import forms, models, permissions
|
||||||
|
|
||||||
|
|
||||||
|
class PersonCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
"""
|
||||||
|
View to create a new instance of :class:`Person`.
|
||||||
|
|
||||||
|
If 'user' is passed as a URL parameter - link the new person to the current user.
|
||||||
|
"""
|
||||||
|
model = models.Person
|
||||||
|
template_name = 'people/person/create.html'
|
||||||
|
form_class = forms.PersonForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if 'user' in self.request.GET:
|
||||||
|
form.instance.user = self.request.user
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class PersonListView(LoginRequiredMixin, ListView):
|
||||||
|
"""
|
||||||
|
View displaying a list of :class:`Person` objects - searchable.
|
||||||
|
"""
|
||||||
|
model = models.Person
|
||||||
|
template_name = 'people/person/list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileView(permissions.UserIsLinkedPersonMixin, 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_object(self, queryset=None) -> models.Person:
|
||||||
|
"""
|
||||||
|
Get the :class:`Person` object to be represented by this page.
|
||||||
|
|
||||||
|
If not determined from url get current user.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return super().get_object(queryset)
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
# pk was not provided in URL
|
||||||
|
return self.request.user.person
|
||||||
|
|
||||||
|
|
||||||
|
class PersonUpdateView(permissions.UserIsLinkedPersonMixin, UpdateView):
|
||||||
|
"""
|
||||||
|
View for updating a :class:`Person` record.
|
||||||
|
"""
|
||||||
|
model = models.Person
|
||||||
|
template_name = 'people/person/update.html'
|
||||||
|
form_class = forms.PersonForm
|
||||||
130
people/views/relationship.py
Normal file
130
people/views/relationship.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
Views for displaying or manipulating instances of :class:`Relationship`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.generic import CreateView, DetailView
|
||||||
|
|
||||||
|
from people import forms, models, permissions
|
||||||
|
|
||||||
|
|
||||||
|
class RelationshipDetailView(permissions.UserIsLinkedPersonMixin, DetailView):
|
||||||
|
"""
|
||||||
|
View displaying details of a :class:`Relationship`.
|
||||||
|
"""
|
||||||
|
model = models.Relationship
|
||||||
|
template_name = 'people/relationship/detail.html'
|
||||||
|
related_person_field = 'source'
|
||||||
|
|
||||||
|
|
||||||
|
class RelationshipCreateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||||
|
"""
|
||||||
|
View for creating a :class:`Relationship`.
|
||||||
|
|
||||||
|
Displays / processes a form containing the :class:`RelationshipQuestion`s.
|
||||||
|
"""
|
||||||
|
model = models.Relationship
|
||||||
|
template_name = 'people/relationship/create.html'
|
||||||
|
fields = [
|
||||||
|
'source',
|
||||||
|
'target',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_test_person(self) -> models.Person:
|
||||||
|
"""
|
||||||
|
Get the person instance which should be used for access control checks.
|
||||||
|
"""
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
return models.Person.objects.get(pk=self.request.POST.get('source'))
|
||||||
|
|
||||||
|
return models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.person = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||||
|
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.person = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||||
|
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
|
||||||
|
initial['source'] = self.request.user.person
|
||||||
|
initial['target'] = self.person
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['person'] = self.person
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('people:relationship.update', kwargs={'relationship_pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, CreateView):
|
||||||
|
"""
|
||||||
|
View for creating a :class:`Relationship`.
|
||||||
|
|
||||||
|
Displays / processes a form containing the :class:`RelationshipQuestion`s.
|
||||||
|
"""
|
||||||
|
model = models.RelationshipAnswerSet
|
||||||
|
template_name = 'people/relationship/update.html'
|
||||||
|
form_class = forms.RelationshipAnswerSetForm
|
||||||
|
|
||||||
|
def get_test_person(self) -> models.Person:
|
||||||
|
"""
|
||||||
|
Get the person instance which should be used for access control checks.
|
||||||
|
"""
|
||||||
|
relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
||||||
|
|
||||||
|
return relationship.source
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
||||||
|
self.person = self.relationship.source
|
||||||
|
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.relationship = models.Relationship.objects.get(pk=self.kwargs.get('relationship_pk'))
|
||||||
|
self.person = self.relationship.source
|
||||||
|
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['person'] = self.person
|
||||||
|
context['relationship'] = self.relationship
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
|
||||||
|
initial['relationship'] = self.relationship
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
answer_set.save()
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
name: mariadb
|
name: mariadb
|
||||||
state: restarted
|
state: restarted
|
||||||
enabled: yes
|
enabled: yes
|
||||||
|
daemon_reload: yes
|
||||||
|
|
||||||
- name: Create database
|
- name: Create database
|
||||||
mysql_db:
|
mysql_db:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ deploy_mode_dict:
|
|||||||
3: Development
|
3: Development
|
||||||
deploy_mode: 3
|
deploy_mode: 3
|
||||||
|
|
||||||
secret_key: '{{ lookup("password", "/tmp/secretkeyfile") }}'
|
secret_key: '{{ lookup("password", "/dev/null") }}'
|
||||||
|
|
||||||
project_name: 'breccia-mapper'
|
project_name: 'breccia-mapper'
|
||||||
project_full_name: 'breccia_mapper'
|
project_full_name: 'breccia_mapper'
|
||||||
|
|||||||
@@ -12,6 +12,17 @@
|
|||||||
name: '*'
|
name: '*'
|
||||||
state: latest
|
state: latest
|
||||||
|
|
||||||
|
- name: Enable RedHat Software Collections - RHEL
|
||||||
|
rhsm_repository:
|
||||||
|
name: rhel-server-rhscl-7-rpms
|
||||||
|
when: ansible_distribution == "RedHat"
|
||||||
|
|
||||||
|
- name: Enable RedHat Software Collections - CentOS
|
||||||
|
yum:
|
||||||
|
name: centos-release-scl
|
||||||
|
state: latest
|
||||||
|
when: ansible_distribution == "CentOS"
|
||||||
|
|
||||||
- name: Install system prerequisites
|
- name: Install system prerequisites
|
||||||
yum:
|
yum:
|
||||||
name: '{{ packages }}'
|
name: '{{ packages }}'
|
||||||
@@ -20,12 +31,8 @@
|
|||||||
packages:
|
packages:
|
||||||
- gcc
|
- gcc
|
||||||
- git
|
- git
|
||||||
- nginx
|
- rh-nginx114
|
||||||
- python36
|
- rh-python36
|
||||||
- python36-devel
|
|
||||||
- python36-pip
|
|
||||||
- python36-setuptools
|
|
||||||
- python36-virtualenv
|
|
||||||
- policycoreutils-python
|
- policycoreutils-python
|
||||||
- python
|
- python
|
||||||
- python-setuptools
|
- python-setuptools
|
||||||
@@ -86,11 +93,15 @@
|
|||||||
group: '{{ web_group }}'
|
group: '{{ web_group }}'
|
||||||
recurse: yes
|
recurse: yes
|
||||||
|
|
||||||
|
- name: Create venv
|
||||||
|
shell: |
|
||||||
|
source scl_source enable rh-python36
|
||||||
|
python3 -m venv {{ venv_dir }}
|
||||||
|
|
||||||
- name: Install pip requirements
|
- name: Install pip requirements
|
||||||
pip:
|
pip:
|
||||||
requirements: '{{ project_dir }}/requirements.txt'
|
requirements: '{{ project_dir }}/requirements.txt'
|
||||||
virtualenv: '{{ venv_dir }}'
|
virtualenv: '{{ venv_dir }}'
|
||||||
virtualenv_command: virtualenv-3
|
|
||||||
|
|
||||||
- name: Create static directory
|
- name: Create static directory
|
||||||
file:
|
file:
|
||||||
@@ -124,10 +135,9 @@
|
|||||||
when: deploy_mode > 1
|
when: deploy_mode > 1
|
||||||
|
|
||||||
- name: Install uWSGI
|
- name: Install uWSGI
|
||||||
pip:
|
shell: |
|
||||||
name: uwsgi
|
source scl_source enable rh-python36
|
||||||
state: latest
|
pip3 install uwsgi
|
||||||
executable: pip3
|
|
||||||
|
|
||||||
- name: Setup uWSGI config
|
- name: Setup uWSGI config
|
||||||
file:
|
file:
|
||||||
@@ -145,6 +155,7 @@
|
|||||||
name: uwsgi
|
name: uwsgi
|
||||||
state: started
|
state: started
|
||||||
enabled: yes
|
enabled: yes
|
||||||
|
daemon_reload: yes
|
||||||
|
|
||||||
- name: Copy web config files
|
- name: Copy web config files
|
||||||
template:
|
template:
|
||||||
@@ -189,7 +200,7 @@
|
|||||||
- name: Copy Nginx site
|
- name: Copy Nginx site
|
||||||
template:
|
template:
|
||||||
src: nginx-site-ssl.j2
|
src: nginx-site-ssl.j2
|
||||||
dest: '/etc/nginx/conf.d/{{ project_name }}-ssl.conf'
|
dest: '/etc/opt/rh/rh-nginx114/nginx/conf.d/{{ project_name }}-ssl.conf'
|
||||||
owner: '{{ web_user }}'
|
owner: '{{ web_user }}'
|
||||||
group: '{{ web_group }}'
|
group: '{{ web_group }}'
|
||||||
|
|
||||||
@@ -198,7 +209,7 @@
|
|||||||
- name: Copy Nginx site
|
- name: Copy Nginx site
|
||||||
template:
|
template:
|
||||||
src: nginx-site.j2
|
src: nginx-site.j2
|
||||||
dest: '/etc/nginx/conf.d/{{ project_name }}.conf'
|
dest: '/etc/opt/rh/rh-nginx114/nginx/conf.d/{{ project_name }}.conf'
|
||||||
owner: '{{ web_user }}'
|
owner: '{{ web_user }}'
|
||||||
group: '{{ web_group }}'
|
group: '{{ web_group }}'
|
||||||
|
|
||||||
@@ -207,9 +218,13 @@
|
|||||||
name: "{{ item }}"
|
name: "{{ item }}"
|
||||||
state: restarted
|
state: restarted
|
||||||
enabled: yes
|
enabled: yes
|
||||||
|
daemon_reload: yes
|
||||||
with_items:
|
with_items:
|
||||||
- uwsgi
|
- uwsgi
|
||||||
- nginx
|
- rh-nginx114-nginx
|
||||||
|
|
||||||
|
- name: Populate service facts
|
||||||
|
service_facts:
|
||||||
|
|
||||||
- name: Open webserver ports on firewall
|
- name: Open webserver ports on firewall
|
||||||
firewalld:
|
firewalld:
|
||||||
@@ -218,6 +233,7 @@
|
|||||||
permanent: yes
|
permanent: yes
|
||||||
immediate: yes
|
immediate: yes
|
||||||
loop:
|
loop:
|
||||||
|
- ssh
|
||||||
- http
|
- http
|
||||||
- https
|
- https
|
||||||
when: vagrant_dir.stat.exists == False
|
when: ansible_facts.services['firewalld.service'] is defined and ansible_facts.services['firewalld.service'].state == 'running'
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
Description=uWSGI Emperor Service
|
Description=uWSGI Emperor Service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown {{ web_user }}:{{ web_group }} /run/uwsgi'
|
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown {{ web_user }}:{{ web_group }} /run/uwsgi; source scl_source rh-python36'
|
||||||
ExecStart=/usr/local/bin/uwsgi --emperor /etc/uwsgi/sites
|
ExecStart=/bin/scl enable rh-python36 "uwsgi --emperor /etc/uwsgi/sites"
|
||||||
Restart=always
|
Restart=always
|
||||||
KillSignal=SIGQUIT
|
KillSignal=SIGQUIT
|
||||||
Type=notify
|
Type=notify
|
||||||
|
|||||||
Reference in New Issue
Block a user