From 42d95beb5a8d545fda0818f8572ffbaa527a5913 Mon Sep 17 00:00:00 2001 From: James Graham Date: Fri, 19 Mar 2021 12:00:32 +0000 Subject: [PATCH] feat: add view to end relationship --- .../0049_relationship_latest_by_timestamp.py | 37 +++++++++++++++++++ .../0050_relationship_remove_timestamps.py | 37 +++++++++++++++++++ people/models/question.py | 5 +++ people/models/relationship.py | 34 +++++++++-------- .../templates/people/person/detail_full.html | 8 ++++ .../people/person/detail_partial.html | 8 ++++ .../person/includes/relationships_full.html | 10 ++++- people/urls.py | 4 ++ people/views/person.py | 11 ++++-- people/views/relationship.py | 25 +++++++++++++ 10 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 people/migrations/0049_relationship_latest_by_timestamp.py create mode 100644 people/migrations/0050_relationship_remove_timestamps.py diff --git a/people/migrations/0049_relationship_latest_by_timestamp.py b/people/migrations/0049_relationship_latest_by_timestamp.py new file mode 100644 index 0000000..b2a2e0b --- /dev/null +++ b/people/migrations/0049_relationship_latest_by_timestamp.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.10 on 2021-03-19 10:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('people', '0048_disciplines_and_organisations'), + ] + + operations = [ + migrations.AlterModelOptions( + name='organisationanswerset', + options={'get_latest_by': 'timestamp', 'ordering': ['timestamp']}, + ), + migrations.AlterModelOptions( + name='organisationrelationship', + options={'get_latest_by': 'created'}, + ), + migrations.AlterModelOptions( + name='organisationrelationshipanswerset', + options={'get_latest_by': 'timestamp', 'ordering': ['timestamp']}, + ), + migrations.AlterModelOptions( + name='personanswerset', + options={'get_latest_by': 'timestamp', 'ordering': ['timestamp']}, + ), + migrations.AlterModelOptions( + name='relationship', + options={'get_latest_by': 'created'}, + ), + migrations.AlterModelOptions( + name='relationshipanswerset', + options={'get_latest_by': 'timestamp', 'ordering': ['timestamp']}, + ), + ] diff --git a/people/migrations/0050_relationship_remove_timestamps.py b/people/migrations/0050_relationship_remove_timestamps.py new file mode 100644 index 0000000..f8ded05 --- /dev/null +++ b/people/migrations/0050_relationship_remove_timestamps.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.10 on 2021-03-19 11:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('people', '0049_relationship_latest_by_timestamp'), + ] + + operations = [ + migrations.AlterModelOptions( + name='organisationrelationship', + options={}, + ), + migrations.RemoveField( + model_name='organisationrelationship', + name='created', + ), + migrations.RemoveField( + model_name='organisationrelationship', + name='expired', + ), + migrations.AlterModelOptions( + name='relationship', + options={}, + ), + migrations.RemoveField( + model_name='relationship', + name='created', + ), + migrations.RemoveField( + model_name='relationship', + name='expired', + ), + ] diff --git a/people/models/question.py b/people/models/question.py index b8df075..4a5fe1a 100644 --- a/people/models/question.py +++ b/people/models/question.py @@ -124,6 +124,7 @@ class AnswerSet(models.Model): ordering = [ 'timestamp', ] + get_latest_by = 'timestamp' #: Entity to which this answer set belongs #: This foreign key must be added to each concrete subclass @@ -145,6 +146,10 @@ class AnswerSet(models.Model): null=True, editable=False) + @property + def is_current(self) -> bool: + return self.replaced_timestamp is None + def as_dict(self, answers: typing.Optional[typing.Dict[str, typing.Any]] = None): """Get the answers from this set as a dictionary for use in Form.initial.""" if answers is None: diff --git a/people/models/relationship.py b/people/models/relationship.py index 8b41553..5123764 100644 --- a/people/models/relationship.py +++ b/people/models/relationship.py @@ -1,6 +1,6 @@ -""" -Models describing relationships between people. -""" +"""Models describing relationships between people.""" + +import typing from django.db import models from django.urls import reverse @@ -59,15 +59,17 @@ class Relationship(models.Model): blank=False, null=False) - #: When was this relationship defined? - created = models.DateTimeField(auto_now_add=True) + @property + def current_answers(self) -> typing.Optional['RelationshipAnswerSet']: + answer_set = self.answer_sets.latest() + if answer_set.is_current: + return answer_set - #: When was this marked as expired? Default None means it has not expired - expired = models.DateTimeField(blank=True, null=True) + return None @property - def current_answers(self) -> 'RelationshipAnswerSet': - return self.answer_sets.last() + def is_current(self) -> bool: + return self.current_answers is not None def get_absolute_url(self): return reverse('people:relationship.detail', kwargs={'pk': self.pk}) @@ -141,15 +143,17 @@ class OrganisationRelationship(models.Model): blank=False, null=False) - #: When was this relationship defined? - created = models.DateTimeField(auto_now_add=True) + @property + def current_answers(self) -> typing.Optional['OrganisationRelationshipAnswerSet']: + answer_set = self.answer_sets.latest() + if answer_set.is_current: + return answer_set - #: When was this marked as expired? Default None means it has not expired - expired = models.DateTimeField(blank=True, null=True) + return None @property - def current_answers(self) -> 'OrganisationRelationshipAnswerSet': - return self.answer_sets.last() + def is_current(self) -> bool: + return self.current_answers is not None def get_absolute_url(self): return reverse('people:organisation.relationship.detail', diff --git a/people/templates/people/person/detail_full.html b/people/templates/people/person/detail_full.html index 39651b0..494e610 100644 --- a/people/templates/people/person/detail_full.html +++ b/people/templates/people/person/detail_full.html @@ -37,6 +37,14 @@ {% endif %} + + {% if relationship %} +
+ End Relationship + +
+ {% endif %}
diff --git a/people/templates/people/person/detail_partial.html b/people/templates/people/person/detail_partial.html index ec9c04c..ff0b785 100644 --- a/people/templates/people/person/detail_partial.html +++ b/people/templates/people/person/detail_partial.html @@ -37,6 +37,14 @@ {% endif %} + + {% if relationship %} +
+ End Relationship + +
+ {% endif %}
diff --git a/people/templates/people/person/includes/relationships_full.html b/people/templates/people/person/includes/relationships_full.html index 336e177..cc67d8b 100644 --- a/people/templates/people/person/includes/relationships_full.html +++ b/people/templates/people/person/includes/relationships_full.html @@ -13,7 +13,15 @@ {% for relationship in person.relationships_as_source.all %} - {{ relationship.target }} + + {% if relationship.is_current %} + {{ relationship.target }} + {% else %} + + {{ relationship.target }} + + {% endif %} + Profile diff --git a/people/urls.py b/people/urls.py index dcd49b6..e38621b 100644 --- a/people/urls.py +++ b/people/urls.py @@ -60,6 +60,10 @@ urlpatterns = [ views.relationship.RelationshipUpdateView.as_view(), name='relationship.update'), + path('relationships//end', + views.relationship.RelationshipEndView.as_view(), + name='relationship.end'), + ################################ # OrganisationRelationship views path('organisations//relationships/create', diff --git a/people/views/person.py b/people/views/person.py index 498abf9..8735f73 100644 --- a/people/views/person.py +++ b/people/views/person.py @@ -44,8 +44,10 @@ class PersonListView(LoginRequiredMixin, ListView): existing_relationships = set() try: existing_relationships = set( - self.request.user.person.relationship_targets.values_list( - 'pk', flat=True)) + self.request.user.person.relationships_as_source.filter( + answer_sets__replaced_timestamp__isnull=True + ).values_list('target_id', flat=True) + ) except ObjectDoesNotExist: # No linked Person yet @@ -132,9 +134,12 @@ class ProfileView(LoginRequiredMixin, DetailView): context['relationship'] = None try: - context['relationship'] = models.Relationship.objects.get( + relationship = models.Relationship.objects.get( source=self.request.user.person, target=self.object) + if relationship.is_current: + context['relationship'] = relationship + except models.Relationship.DoesNotExist: pass diff --git a/people/views/relationship.py b/people/views/relationship.py index 692b9c4..3e638ec 100644 --- a/people/views/relationship.py +++ b/people/views/relationship.py @@ -6,6 +6,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from django.utils import timezone from django.views.generic import DetailView, RedirectView, UpdateView +from django.views.generic.detail import SingleObjectMixin from people import forms, models, permissions @@ -91,6 +92,30 @@ class RelationshipUpdateView(permissions.UserIsLinkedPersonMixin, UpdateView): return self.object.get_absolute_url() +class RelationshipEndView(permissions.UserIsLinkedPersonMixin, + SingleObjectMixin, RedirectView): + """View for marking a relationship as ended. + + Sets `replaced_timestamp` on all answer sets where this is currently null. + """ + model = models.Relationship + + def get_test_person(self) -> models.Person: + """Get the person instance which should be used for access control checks.""" + return self.get_object().source + + def get_redirect_url(self, *args, **kwargs): + """Mark any previous answer sets as replaced.""" + now_date = timezone.now().date() + relationship = self.get_object() + + relationship.answer_sets.filter( + replaced_timestamp__isnull=True).update( + replaced_timestamp=now_date) + + return relationship.target.get_absolute_url() + + class OrganisationRelationshipDetailView(permissions.UserIsLinkedPersonMixin, DetailView): """View displaying details of an :class:`OrganisationRelationship`."""