mirror of
https://github.com/Southampton-RSG/breccia-mapper.git
synced 2026-03-03 03:17:07 +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
|
||||
/.dbbackup/
|
||||
.vagrant/
|
||||
staging.yml
|
||||
/.dbbackup/
|
||||
production.yml
|
||||
|
||||
4
Makefile
4
Makefile
@@ -9,8 +9,8 @@ lint:
|
||||
|
||||
.PHONY: 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
|
||||
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
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse
|
||||
from django.views.generic import DetailView, ListView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
@@ -12,7 +13,7 @@ from people import permissions
|
||||
from . import models
|
||||
|
||||
|
||||
class ActivitySeriesListView(ListView):
|
||||
class ActivitySeriesListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
View displaying a list of :class:`ActivitySeries`.
|
||||
"""
|
||||
@@ -21,7 +22,7 @@ class ActivitySeriesListView(ListView):
|
||||
context_object_name = 'activity_series_list'
|
||||
|
||||
|
||||
class ActivitySeriesDetailView(DetailView):
|
||||
class ActivitySeriesDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View displaying details of a single :class:`ActivitySeries`.
|
||||
"""
|
||||
@@ -30,7 +31,7 @@ class ActivitySeriesDetailView(DetailView):
|
||||
context_object_name = 'activity_series'
|
||||
|
||||
|
||||
class ActivityListView(ListView):
|
||||
class ActivityListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
View displaying a list of :class:`Activity`.
|
||||
"""
|
||||
@@ -38,7 +39,7 @@ class ActivityListView(ListView):
|
||||
template_name = 'activities/activity/list.html'
|
||||
|
||||
|
||||
class ActivityDetailView(DetailView):
|
||||
class ActivityDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View displaying details of a single :class:`Activity`.
|
||||
"""
|
||||
|
||||
@@ -77,6 +77,7 @@ THIRD_PARTY_APPS = [
|
||||
FIRST_PARTY_APPS = [
|
||||
'people',
|
||||
'activities',
|
||||
'export',
|
||||
]
|
||||
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + FIRST_PARTY_APPS
|
||||
@@ -141,6 +142,11 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'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>
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item">
|
||||
<a href="{% url 'export:index' %}" class="nav-link">Export</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="{% url 'admin:index' %}" class="nav-link">Admin</a>
|
||||
</li>
|
||||
|
||||
@@ -28,6 +28,9 @@ urlpatterns = [
|
||||
views.IndexView.as_view(),
|
||||
name='index'),
|
||||
|
||||
path('',
|
||||
include('export.urls')),
|
||||
|
||||
path('',
|
||||
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
|
||||
|
||||
|
||||
|
||||
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.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from .person import Person
|
||||
|
||||
@@ -42,12 +43,16 @@ class RelationshipQuestion(models.Model):
|
||||
@property
|
||||
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 [
|
||||
[choice.pk, str(choice)] for choice in self.answers.all()
|
||||
]
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
return slugify(self.text)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
|
||||
@@ -80,6 +85,10 @@ class RelationshipQuestionChoice(models.Model):
|
||||
order = models.SmallIntegerField(default=0,
|
||||
blank=False, null=False)
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
return slugify(self.text)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ class PersonSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class RelationshipSerializer(serializers.ModelSerializer):
|
||||
source = PersonSerializer()
|
||||
target = PersonSerializer()
|
||||
|
||||
class Meta:
|
||||
model = models.Relationship
|
||||
fields = [
|
||||
|
||||
@@ -7,46 +7,38 @@ app_name = 'people'
|
||||
|
||||
urlpatterns = [
|
||||
path('profile/',
|
||||
views.ProfileView.as_view(),
|
||||
views.person.ProfileView.as_view(),
|
||||
name='person.profile'),
|
||||
|
||||
path('people/create',
|
||||
views.PersonCreateView.as_view(),
|
||||
views.person.PersonCreateView.as_view(),
|
||||
name='person.create'),
|
||||
|
||||
path('people',
|
||||
views.PersonListView.as_view(),
|
||||
views.person.PersonListView.as_view(),
|
||||
name='person.list'),
|
||||
|
||||
path('people/<int:pk>',
|
||||
views.ProfileView.as_view(),
|
||||
views.person.ProfileView.as_view(),
|
||||
name='person.detail'),
|
||||
|
||||
path('people/<int:pk>/update',
|
||||
views.PersonUpdateView.as_view(),
|
||||
views.person.PersonUpdateView.as_view(),
|
||||
name='person.update'),
|
||||
|
||||
path('people/<int:person_pk>/relationships/create',
|
||||
views.RelationshipCreateView.as_view(),
|
||||
views.relationship.RelationshipCreateView.as_view(),
|
||||
name='person.relationship.create'),
|
||||
|
||||
path('relationships/<int:pk>',
|
||||
views.RelationshipDetailView.as_view(),
|
||||
views.relationship.RelationshipDetailView.as_view(),
|
||||
name='relationship.detail'),
|
||||
|
||||
path('relationships/<int:relationship_pk>/update',
|
||||
views.RelationshipUpdateView.as_view(),
|
||||
views.relationship.RelationshipUpdateView.as_view(),
|
||||
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',
|
||||
views.NetworkView.as_view(),
|
||||
views.network.NetworkView.as_view(),
|
||||
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
|
||||
state: restarted
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Create database
|
||||
mysql_db:
|
||||
|
||||
@@ -5,7 +5,7 @@ deploy_mode_dict:
|
||||
3: Development
|
||||
deploy_mode: 3
|
||||
|
||||
secret_key: '{{ lookup("password", "/tmp/secretkeyfile") }}'
|
||||
secret_key: '{{ lookup("password", "/dev/null") }}'
|
||||
|
||||
project_name: 'breccia-mapper'
|
||||
project_full_name: 'breccia_mapper'
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
name: '*'
|
||||
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
|
||||
yum:
|
||||
name: '{{ packages }}'
|
||||
@@ -20,12 +31,8 @@
|
||||
packages:
|
||||
- gcc
|
||||
- git
|
||||
- nginx
|
||||
- python36
|
||||
- python36-devel
|
||||
- python36-pip
|
||||
- python36-setuptools
|
||||
- python36-virtualenv
|
||||
- rh-nginx114
|
||||
- rh-python36
|
||||
- policycoreutils-python
|
||||
- python
|
||||
- python-setuptools
|
||||
@@ -86,11 +93,15 @@
|
||||
group: '{{ web_group }}'
|
||||
recurse: yes
|
||||
|
||||
- name: Create venv
|
||||
shell: |
|
||||
source scl_source enable rh-python36
|
||||
python3 -m venv {{ venv_dir }}
|
||||
|
||||
- name: Install pip requirements
|
||||
pip:
|
||||
requirements: '{{ project_dir }}/requirements.txt'
|
||||
virtualenv: '{{ venv_dir }}'
|
||||
virtualenv_command: virtualenv-3
|
||||
|
||||
- name: Create static directory
|
||||
file:
|
||||
@@ -124,10 +135,9 @@
|
||||
when: deploy_mode > 1
|
||||
|
||||
- name: Install uWSGI
|
||||
pip:
|
||||
name: uwsgi
|
||||
state: latest
|
||||
executable: pip3
|
||||
shell: |
|
||||
source scl_source enable rh-python36
|
||||
pip3 install uwsgi
|
||||
|
||||
- name: Setup uWSGI config
|
||||
file:
|
||||
@@ -145,6 +155,7 @@
|
||||
name: uwsgi
|
||||
state: started
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Copy web config files
|
||||
template:
|
||||
@@ -189,7 +200,7 @@
|
||||
- name: Copy Nginx site
|
||||
template:
|
||||
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 }}'
|
||||
group: '{{ web_group }}'
|
||||
|
||||
@@ -198,7 +209,7 @@
|
||||
- name: Copy Nginx site
|
||||
template:
|
||||
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 }}'
|
||||
group: '{{ web_group }}'
|
||||
|
||||
@@ -207,9 +218,13 @@
|
||||
name: "{{ item }}"
|
||||
state: restarted
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
with_items:
|
||||
- uwsgi
|
||||
- nginx
|
||||
- rh-nginx114-nginx
|
||||
|
||||
- name: Populate service facts
|
||||
service_facts:
|
||||
|
||||
- name: Open webserver ports on firewall
|
||||
firewalld:
|
||||
@@ -218,6 +233,7 @@
|
||||
permanent: yes
|
||||
immediate: yes
|
||||
loop:
|
||||
- ssh
|
||||
- http
|
||||
- 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
|
||||
|
||||
[Service]
|
||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown {{ web_user }}:{{ web_group }} /run/uwsgi'
|
||||
ExecStart=/usr/local/bin/uwsgi --emperor /etc/uwsgi/sites
|
||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown {{ web_user }}:{{ web_group }} /run/uwsgi; source scl_source rh-python36'
|
||||
ExecStart=/bin/scl enable rh-python36 "uwsgi --emperor /etc/uwsgi/sites"
|
||||
Restart=always
|
||||
KillSignal=SIGQUIT
|
||||
Type=notify
|
||||
|
||||
Reference in New Issue
Block a user