Merge branch 'dev'

This commit is contained in:
James Graham
2020-04-02 11:10:00 +01:00
31 changed files with 686 additions and 320 deletions

3
.gitignore vendored
View File

@@ -16,6 +16,7 @@ deployment-key
deployment-key.pub deployment-key.pub
# Deployment # Deployment
/.dbbackup/
.vagrant/ .vagrant/
staging.yml staging.yml
/.dbbackup/ production.yml

View File

@@ -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

View File

@@ -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`.
""" """

View File

@@ -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',
],
} }

View File

@@ -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>

View File

@@ -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')),

View File

@@ -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
View File

5
export/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ExportConfig(AppConfig):
name = 'export'

View File

@@ -0,0 +1,4 @@
from . import (
activities,
people
)

View 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',
]

View 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

View 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

View 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
View 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
View File

@@ -0,0 +1,6 @@
from .base import ExportListView
from . import (
activities,
people
)

View 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
View 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
View 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

View File

@@ -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

View File

@@ -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 = [

View File

@@ -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'),
] ]

View File

@@ -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
View 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
View 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
View 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

View 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

View File

@@ -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:

View File

@@ -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'

View File

@@ -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'

View File

@@ -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