From 3aaa9dd9863c5d597d6bead45530dd8dbb2e3984 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 14 Jul 2021 09:57:13 +0200 Subject: [PATCH 1/7] create subject/contact creation request models --- castellum/contacts/admin.py | 2 + .../migrations/0010_contactcreationrequest.py | 32 ++++++++++++ castellum/contacts/models.py | 52 +++++++++++-------- castellum/subjects/admin.py | 1 + .../migrations/0026_subjectcreationrequest.py | 26 ++++++++++ castellum/subjects/models.py | 14 +++++ tests/subjects/views/test_subject_export.py | 2 + 7 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 castellum/contacts/migrations/0010_contactcreationrequest.py create mode 100644 castellum/subjects/migrations/0026_subjectcreationrequest.py diff --git a/castellum/contacts/admin.py b/castellum/contacts/admin.py index 8f9344955..be0886ca6 100644 --- a/castellum/contacts/admin.py +++ b/castellum/contacts/admin.py @@ -23,6 +23,7 @@ from django.contrib import admin from .models import Address from .models import Contact +from .models import ContactCreationRequest from .models import Street @@ -60,4 +61,5 @@ class ContactAdmin(admin.ModelAdmin): admin.site.register(Contact, ContactAdmin) +admin.site.register(ContactCreationRequest) admin.site.register(Street) diff --git a/castellum/contacts/migrations/0010_contactcreationrequest.py b/castellum/contacts/migrations/0010_contactcreationrequest.py new file mode 100644 index 000000000..9e097bd86 --- /dev/null +++ b/castellum/contacts/migrations/0010_contactcreationrequest.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.6 on 2021-08-16 15:46 + +import castellum.utils.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contacts', '0009_phonetic_diacritics'), + ] + + operations = [ + migrations.CreateModel( + name='ContactCreationRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateField(auto_now=True, verbose_name='Updated at')), + ('subject_id', models.PositiveIntegerField(default=None, editable=False, unique=True)), + ('first_name', models.CharField(max_length=64, verbose_name='First name')), + ('last_name', models.CharField(max_length=64, verbose_name='Last name')), + ('title', models.CharField(blank=True, max_length=64, verbose_name='Title')), + ('gender', models.CharField(blank=True, choices=[('f', 'female'), ('m', 'male'), ('*', 'diverse')], max_length=1, verbose_name='Gender')), + ('date_of_birth', castellum.utils.fields.DateField(blank=True, null=True, verbose_name='Date of birth')), + ('email', models.EmailField(max_length=128, verbose_name='Email')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/castellum/contacts/models.py b/castellum/contacts/models.py index be67c65fc..0082a30bd 100644 --- a/castellum/contacts/models.py +++ b/castellum/contacts/models.py @@ -123,30 +123,48 @@ class ContactQuerySet(models.QuerySet): return q -class Contact(TimeStampedModel): +class BaseContact(TimeStampedModel): GENDER = [ ("f", _("female")), ("m", _("male")), ("*", _("diverse")), ] - CONTACT_METHODS = [ - ("phone", _("phone")), - ("email", _("email")), - ("postal", _("postal")), - ] subject_id = models.PositiveIntegerField(unique=True, editable=False, default=None) first_name = models.CharField(_('First name'), max_length=64) - first_name_phonetic = models.CharField(max_length=128, editable=False, default=None) last_name = models.CharField(_('Last name'), max_length=64) - last_name_phonetic = models.CharField(max_length=128, editable=False, default=None) title = models.CharField(_('Title'), max_length=64, blank=True) gender = models.CharField(_('Gender'), max_length=1, choices=GENDER, blank=True) - date_of_birth = DateField(_('Date of birth'), blank=True, null=True) + class Meta: + abstract = True + + def __str__(self): + return self.full_name + + @property + def full_name(self): + return " ".join(filter(None, [self.title, self.first_name, self.last_name])) + + @property + def short_name(self): + # a bit simplistic, but should be sufficient + return '{}. {}'.format(self.first_name[0], self.last_name) + + +class Contact(BaseContact): + CONTACT_METHODS = [ + ("phone", _("phone")), + ("email", _("email")), + ("postal", _("postal")), + ] + + first_name_phonetic = models.CharField(max_length=128, editable=False, default=None) + last_name_phonetic = models.CharField(max_length=128, editable=False, default=None) + email = models.EmailField(_('Email'), max_length=128, blank=True) phone_number = PhoneNumberField(_('Phone number'), max_length=32, blank=True) phone_number_alternative = PhoneNumberField( @@ -168,9 +186,6 @@ class Contact(TimeStampedModel): verbose_name = _('Contact data') verbose_name_plural = _('Contact data') - def __str__(self): - return self.full_name - def get_address(self): try: return self.address @@ -196,15 +211,6 @@ class Contact(TimeStampedModel): [c for c in self.guardians.all() if not c.subject.blocked], ]) - @property - def full_name(self): - return " ".join(filter(None, [self.title, self.first_name, self.last_name])) - - @property - def short_name(self): - # a bit simplistic, but should be sufficient - return '{}. {}'.format(self.first_name[0], self.last_name) - @cached_property def is_guardian(self): return self.id and self.guardian_of.exists() @@ -270,3 +276,7 @@ class Street(models.Model): def __str__(self): return self.name + + +class ContactCreationRequest(BaseContact): + email = models.EmailField(_('Email'), max_length=128) diff --git a/castellum/subjects/admin.py b/castellum/subjects/admin.py index f17d37aea..377050415 100644 --- a/castellum/subjects/admin.py +++ b/castellum/subjects/admin.py @@ -33,6 +33,7 @@ class ConsentDocumentAdmin(admin.ModelAdmin): admin.site.register(models.Subject, SubjectAdmin) +admin.site.register(models.SubjectCreationRequest) admin.site.register(models.SubjectNote) admin.site.register(models.Consent) admin.site.register(models.ConsentDocument, ConsentDocumentAdmin) diff --git a/castellum/subjects/migrations/0026_subjectcreationrequest.py b/castellum/subjects/migrations/0026_subjectcreationrequest.py new file mode 100644 index 000000000..241f275fa --- /dev/null +++ b/castellum/subjects/migrations/0026_subjectcreationrequest.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.7 on 2021-09-21 11:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('subjects', '0025_rm_availability'), + ] + + operations = [ + migrations.CreateModel( + name='SubjectCreationRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateField(auto_now=True, verbose_name='Updated at')), + ('external_id', models.IntegerField(blank=True, null=True, unique=True, verbose_name='External ID')), + ('source', models.CharField(blank=True, max_length=128, verbose_name='Data source')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/castellum/subjects/models.py b/castellum/subjects/models.py index 896dadfea..cf29e8807 100644 --- a/castellum/subjects/models.py +++ b/castellum/subjects/models.py @@ -247,3 +247,17 @@ class ExportAnswer(models.Model): def __str__(self): return str(self.created_at) + + +class SubjectCreationRequest(TimeStampedModel): + external_id = models.IntegerField(_('External ID'), blank=True, null=True, unique=True) + source = models.CharField(_('Data source'), max_length=128, blank=True) + + @cached_property + def contact(self): + from castellum.contacts.models import ContactCreationRequest + return ContactCreationRequest.objects.get(subject_id=self.pk) + + def delete(self): + self.contact.delete() + return super().delete() diff --git a/tests/subjects/views/test_subject_export.py b/tests/subjects/views/test_subject_export.py index b078dd962..0a429c495 100644 --- a/tests/subjects/views/test_subject_export.py +++ b/tests/subjects/views/test_subject_export.py @@ -23,6 +23,7 @@ EXCLUDED = [ 'contacts.Contact.id', 'contacts.Contact.last_name_phonetic', 'contacts.Contact.subject_id', + 'contacts.ContactCreationRequest.', 'contacts.Street.', 'contenttypes.', 'geofilters.Geolocation.', @@ -54,6 +55,7 @@ EXCLUDED = [ 'subjects.Subject.to_be_deleted_notified', 'subjects.Subject.pseudonym', 'subjects.Subject.slug', + 'subjects.SubjectCreationRequest.', # already covered by other fields (e.g. backrefs) 'subjects.SubjectNote.', -- GitLab From 7213e7256ec92cec8b58498507c0b138e0c2ea76 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 21 Jul 2021 15:29:03 +0200 Subject: [PATCH 2/7] create maintenance view --- .../templates/subjects/maintenance_base.html | 3 ++ .../maintenance_subjectcreationrequest.html | 28 +++++++++++++++++++ castellum/subjects/urls.py | 6 ++++ castellum/subjects/views.py | 9 ++++++ 4 files changed, 46 insertions(+) create mode 100644 castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html diff --git a/castellum/subjects/templates/subjects/maintenance_base.html b/castellum/subjects/templates/subjects/maintenance_base.html index 286378706..8e76aab09 100644 --- a/castellum/subjects/templates/subjects/maintenance_base.html +++ b/castellum/subjects/templates/subjects/maintenance_base.html @@ -28,6 +28,9 @@ + diff --git a/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html b/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html new file mode 100644 index 000000000..a2dedf8fc --- /dev/null +++ b/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html @@ -0,0 +1,28 @@ +{% extends "subjects/maintenance_base.html" %} +{% load i18n bootstrap4 auth %} + +{% block title %}{% translate 'Subject creation requests' %} · {{ block.super }}{% endblock %} + +{% block content %} +

{% translate 'The following people would like to be added to the database:' %}

+ + + + {% if is_paginated %} + + {% endif %} +{% endblock %} diff --git a/castellum/subjects/urls.py b/castellum/subjects/urls.py index 74472dd9a..eb47a733f 100644 --- a/castellum/subjects/urls.py +++ b/castellum/subjects/urls.py @@ -33,6 +33,7 @@ from .views import MaintenanceContactView from .views import MaintenanceDuplicatesView from .views import MaintenanceNotesView from .views import MaintenanceReliabilityView +from .views import MaintenanceSubjectCreationRequestView from .views import MaintenanceWaitingView from .views import ParticipationAddView from .views import ParticipationDeleteView @@ -67,6 +68,11 @@ urlpatterns = [ MaintenanceReliabilityView.as_view(), name='maintenance-reliability', ), + path( + 'maintenance/subjectcreationrequest/', + MaintenanceSubjectCreationRequestView.as_view(), + name='maintenance-subjectcreationrequest', + ), path('maintenance/notes/', MaintenanceNotesView.as_view(), name='maintenance-notes'), path('/', SubjectDetailView.as_view(), name='detail'), path('/pseudonyms/', SubjectPseudonymsView.as_view(), name='pseudonyms'), diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index 5854bf290..07c5b6e37 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -70,6 +70,7 @@ from .mixins import SubjectMixin from .models import Consent from .models import ExportAnswer from .models import Subject +from .models import SubjectCreationRequest monitoring_logger = logging.getLogger('monitoring.subjects') @@ -702,6 +703,14 @@ class MaintenanceReliabilityView(BaseMaintenanceView): ) +class MaintenanceSubjectCreationRequestView(PermissionRequiredMixin, ListView): + model = SubjectCreationRequest + paginate_by = 20 + permission_required = 'subjects.change_subject' + template_name = 'subjects/maintenance_subjectcreationrequest.html' + tab = 'subjectcreationrequest' + + class MaintenanceNotesView(BaseMaintenanceView): template_name = 'subjects/maintenance_notes.html' tab = 'notes' -- GitLab From ae5923f125e2c75b6c78084c56b94b8af26b6981 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 21 Jul 2021 15:51:24 +0200 Subject: [PATCH 3/7] add delete view --- .../maintenance_subjectcreationrequest.html | 1 + .../subjects/subjectdraft_confirm_delete.html | 15 +++++++++++++++ castellum/subjects/urls.py | 6 ++++++ castellum/subjects/views.py | 9 +++++++++ 4 files changed, 31 insertions(+) create mode 100644 castellum/subjects/templates/subjects/subjectdraft_confirm_delete.html diff --git a/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html b/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html index a2dedf8fc..aa0bf6065 100644 --- a/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html +++ b/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html @@ -13,6 +13,7 @@ {{ subjectcreationrequest.contact }} {{ subjectcreationrequest.contact.email }} {% empty %} diff --git a/castellum/subjects/templates/subjects/subjectdraft_confirm_delete.html b/castellum/subjects/templates/subjects/subjectdraft_confirm_delete.html new file mode 100644 index 000000000..94ba40fbd --- /dev/null +++ b/castellum/subjects/templates/subjects/subjectdraft_confirm_delete.html @@ -0,0 +1,15 @@ +{% extends "subjects/maintenance_base.html" %} +{% load static i18n %} + +{% block title %}{% translate "Discard subject creation request" %} · {{ block.super }}{% endblock %} + +{% block content %} +
+ {% csrf_token %} +

{% translate "Are you sure you want to permanently delete this subject creation request?" %}

+
+ {% translate 'Cancel' %} + +
+
+{% endblock %} diff --git a/castellum/subjects/urls.py b/castellum/subjects/urls.py index eb47a733f..67725bbdf 100644 --- a/castellum/subjects/urls.py +++ b/castellum/subjects/urls.py @@ -33,6 +33,7 @@ from .views import MaintenanceContactView from .views import MaintenanceDuplicatesView from .views import MaintenanceNotesView from .views import MaintenanceReliabilityView +from .views import MaintenanceSubjectCreationRequestDeleteView from .views import MaintenanceSubjectCreationRequestView from .views import MaintenanceWaitingView from .views import ParticipationAddView @@ -73,6 +74,11 @@ urlpatterns = [ MaintenanceSubjectCreationRequestView.as_view(), name='maintenance-subjectcreationrequest', ), + path( + 'maintenance/subjectcreationrequest//delete/', + MaintenanceSubjectCreationRequestDeleteView.as_view(), + name='maintenance-subjectcreationrequest-delete', + ), path('maintenance/notes/', MaintenanceNotesView.as_view(), name='maintenance-notes'), path('/', SubjectDetailView.as_view(), name='detail'), path('/pseudonyms/', SubjectPseudonymsView.as_view(), name='pseudonyms'), diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index 07c5b6e37..b38d29ee4 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -711,6 +711,15 @@ class MaintenanceSubjectCreationRequestView(PermissionRequiredMixin, ListView): tab = 'subjectcreationrequest' +class MaintenanceSubjectCreationRequestDeleteView(PermissionRequiredMixin, DeleteView): + model = SubjectCreationRequest + permission_required = 'subjects.change_subject' + tab = 'subjectcreationrequest' + + def get_success_url(self): + return reverse('subjects:maintenance-subjectcreationrequest') + + class MaintenanceNotesView(BaseMaintenanceView): template_name = 'subjects/maintenance_notes.html' tab = 'notes' -- GitLab From d1c93a7825924c4d1bdbda23a981a28dc46e1925 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 21 Jul 2021 15:55:54 +0200 Subject: [PATCH 4/7] use data from creation request in subject create view --- castellum/contacts/models.py | 10 ++++++ castellum/subjects/models.py | 7 ++++ .../maintenance_subjectcreationrequest.html | 5 +++ castellum/subjects/views.py | 32 +++++++++++++------ .../views/test_subject_search_view.py | 19 +++++++++++ 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/castellum/contacts/models.py b/castellum/contacts/models.py index 0082a30bd..19fcf4dbc 100644 --- a/castellum/contacts/models.py +++ b/castellum/contacts/models.py @@ -280,3 +280,13 @@ class Street(models.Model): class ContactCreationRequest(BaseContact): email = models.EmailField(_('Email'), max_length=128) + + def convert_to_real_contact(self): + return Contact.objects.create( + first_name=self.first_name, + last_name=self.last_name, + title=self.title, + gender=self.gender, + date_of_birth=self.date_of_birth, + email=self.email, + ) diff --git a/castellum/subjects/models.py b/castellum/subjects/models.py index cf29e8807..852fc9778 100644 --- a/castellum/subjects/models.py +++ b/castellum/subjects/models.py @@ -261,3 +261,10 @@ class SubjectCreationRequest(TimeStampedModel): def delete(self): self.contact.delete() return super().delete() + + def convert_to_real_subject(self): + contact = self.contact.convert_to_real_contact() + subject = contact.subject + subject.source = self.source + subject.save() + return subject diff --git a/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html b/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html index aa0bf6065..213642432 100644 --- a/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html +++ b/castellum/subjects/templates/subjects/maintenance_subjectcreationrequest.html @@ -13,6 +13,11 @@ {{ subjectcreationrequest.contact }} {{ subjectcreationrequest.contact.email }}
+
+ {% csrf_token %} + + +
{% translate 'Discard' %}
diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index b38d29ee4..ea17533fa 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -80,6 +80,12 @@ class SubjectSearchView(LoginRequiredMixin, FormView): template_name = 'subjects/subject_search.html' form_class = SearchForm + @cached_property + def subjectcreationrequest(self): + if 'from-request' in self.request.GET: + pk = self.request.GET['from-request'] + return get_object_or_404(SubjectCreationRequest, pk=pk) + def get_matches(self, search): user = self.request.user contacts = Contact.objects.fuzzy_filter(search) @@ -160,19 +166,25 @@ class SubjectSearchView(LoginRequiredMixin, FormView): if form.is_valid() and privacy_level_form.is_valid(): if form.cleaned_data.get('last_name'): - contact = Contact.objects.create( - first_name=form.cleaned_data['first_name'], - last_name=form.cleaned_data['last_name'], - phone_number=form.cleaned_data['phone_number'], - email=form.cleaned_data.get('email'), - ) - contact.subject.privacy_level = privacy_level_form.cleaned_data['privacy_level'] - contact.subject.save() + if 'from-request' in self.request.GET: + subject = self.subjectcreationrequest.convert_to_real_subject() + self.subjectcreationrequest.delete() + else: + contact = Contact.objects.create( + first_name=form.cleaned_data['first_name'], + last_name=form.cleaned_data['last_name'], + phone_number=form.cleaned_data['phone_number'], + email=form.cleaned_data.get('email'), + ) + subject = contact.subject + + subject.privacy_level = privacy_level_form.cleaned_data['privacy_level'] + subject.save() monitoring_logger.info('SubjectData update: {} by {}'.format( - contact.subject_id, self.request.user.pk + subject.id, self.request.user.pk )) - return redirect('subjects:detail', slug=contact.subject.slug) + return redirect('subjects:detail', slug=subject.slug) else: messages.error(request, _( 'Both first and last name must be provided in order to create a subject!' diff --git a/tests/subjects/views/test_subject_search_view.py b/tests/subjects/views/test_subject_search_view.py index 525425175..9b060f385 100644 --- a/tests/subjects/views/test_subject_search_view.py +++ b/tests/subjects/views/test_subject_search_view.py @@ -2,7 +2,9 @@ import pytest from model_bakery import baker from castellum.contacts.models import Contact +from castellum.contacts.models import ContactCreationRequest from castellum.recruitment.models import Participation +from castellum.subjects.models import SubjectCreationRequest @pytest.mark.smoketest @@ -149,3 +151,20 @@ def test_create_no_search(client, user): client.force_login(user) response = client.post('/subjects/', {'privacy_level': 0}) assert response.status_code == 200 + + +def test_create_from_subjectcreationrequest(client, user): + subject_request = baker.make(SubjectCreationRequest) + contact_request = baker.make(ContactCreationRequest, subject_id=subject_request.pk) + + client.force_login(user) + response = client.post('/subjects/?from-request={}'.format(subject_request.pk), { + 'search': '{} {}'.format(contact_request.first_name, contact_request.last_name), + 'privacy_level': 0, + }) + + assert response.status_code == 302 + assert not SubjectCreationRequest.objects.exists() + assert not ContactCreationRequest.objects.exists() + assert Contact.objects.exists() + assert Contact.objects.get().first_name == contact_request.first_name -- GitLab From 0f5e0381ef6439729051ed91c5ea70d001fe9180 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 14 Jul 2021 11:21:27 +0200 Subject: [PATCH 5/7] show creation request info in create form --- .../templates/subjects/subject_search.html | 17 +++++++++++------ castellum/subjects/views.py | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/castellum/subjects/templates/subjects/subject_search.html b/castellum/subjects/templates/subjects/subject_search.html index f9497b9ef..40884d202 100644 --- a/castellum/subjects/templates/subjects/subject_search.html +++ b/castellum/subjects/templates/subjects/subject_search.html @@ -130,17 +130,22 @@ {% endfor %} {% csrf_token %} -
-
{% translate 'First name' %}
-
{{ form.cleaned_data.first_name }}
+ {% if subjectcreationrequest %} +

{% blocktranslate with name=subjectcreationrequest.contact.full_name %}Information will be taken from subject creation request "{{ name }}".{% endblocktranslate %}

+ {% else %} +
+
{% translate 'First name' %}
+
{{ form.cleaned_data.first_name }}
-
{% translate 'Last name' %}
-
{{ form.cleaned_data.last_name }}
-
+
{% translate 'Last name' %}
+
{{ form.cleaned_data.last_name }}
+
+ {% endif %} {% bootstrap_field privacy_level_form.privacy_level %} {{ form.search.as_hidden }} + {% endif %} diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index ea17533fa..b2e46a3f3 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -139,6 +139,8 @@ class SubjectSearchView(LoginRequiredMixin, FormView): ).values('subject_id') ).count() + context['subjectcreationrequest'] = self.subjectcreationrequest + form = kwargs.get('form') if form and form.is_valid(): context['matches'] = self.get_matches(form.cleaned_data['search']) -- GitLab From 7986e14e9b5eef14287eaf3b272d2a751c67ebac Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 14 Jul 2021 11:42:43 +0200 Subject: [PATCH 6/7] make CASTELLUM_DATE_OF_BIRTH_ATTRIBUTE_ID work with creation requests --- castellum/subjects/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/castellum/subjects/models.py b/castellum/subjects/models.py index 852fc9778..4d0d49ca2 100644 --- a/castellum/subjects/models.py +++ b/castellum/subjects/models.py @@ -267,4 +267,6 @@ class SubjectCreationRequest(TimeStampedModel): subject = contact.subject subject.source = self.source subject.save() + # CASTELLUM_DATE_OF_BIRTH_ATTRIBUTE_ID is synced on contact save + contact.save() return subject -- GitLab From eeff9b647de9953fc06a359e58cddc3987aee7a3 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 26 Jul 2021 16:51:58 +0200 Subject: [PATCH 7/7] Provide command to fetch SubjectCreationRequests --- .../fetch_subject_creation_requests.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 castellum/subjects/management/commands/fetch_subject_creation_requests.py diff --git a/castellum/subjects/management/commands/fetch_subject_creation_requests.py b/castellum/subjects/management/commands/fetch_subject_creation_requests.py new file mode 100644 index 000000000..034dce50b --- /dev/null +++ b/castellum/subjects/management/commands/fetch_subject_creation_requests.py @@ -0,0 +1,117 @@ +# (c) 2018-2021 +# MPIB , +# MPI-CBS , +# MPIP +# +# This file is part of Castellum. +# +# Castellum is free software; you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Castellum is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with Castellum. If not, see +# . + +""" +Depending on the service that is used to gather SubjectCreationRequests +this script may not be flexible enough. In that case it can still serve +as an example. +""" + +from django.core.management.base import BaseCommand +from django.forms import modelform_factory + +import requests + +from castellum.contacts.models import ContactCreationRequest +from castellum.subjects.models import SubjectCreationRequest + +SubjectForm = modelform_factory(SubjectCreationRequest, fields=['external_id', 'source']) +ContactForm = modelform_factory(ContactCreationRequest, fields=[ + 'first_name', 'last_name', 'title', 'gender', 'date_of_birth', 'email' +]) + + +def import_creationrequest(data): + subject_form = SubjectForm(data) + contact_form = ContactForm(data) + + subject_valid = subject_form.is_valid() + contact_valid = contact_form.is_valid() + + if subject_valid and contact_valid: + subjectcreationrequest = subject_form.save() + contact_form.instance.subject_id = subjectcreationrequest.pk + contact_form.save() + return subjectcreationrequest + else: + raise ValueError({**subject_form.errors, **contact_form.errors}) + + +def do_import(url, token): + r = requests.get(url, headers={'Authorization': 'token ' + token}) + r.raise_for_status() + + success = [] + errors = [] + + for item in r.json(): + try: + success.append(import_creationrequest(item)) + except Exception as e: + errors.append(e) + + return success, errors + + +def do_cleanup_remote(url_template, token): + success = [] + errors = [] + + for subject in SubjectCreationRequest.objects.exclude(external_id=''): + url = url_template.format(subject.external_id) + r = requests.delete(url, headers={'Authorization': 'token ' + token}) + if r.status_code in [201, 404]: + subject.external_id = '' + subject.save() + success.append(subject) + else: + errors.append(r) + + return success, errors + + +class Command(BaseCommand): + help = 'Fetch SubjectCreationRequests from an external service.' + + def add_arguments(self, parser): + parser.add_argument('url', help='URL to a list of subject creation requests (JSON)') + parser.add_argument('--token', help='Authentication token') + parser.add_argument('--delete-url', help=( + 'URL that will delete a subject creation request on DELETE. ' + 'Use `{}` as a placeholder for the ID.' + )) + + def handle(self, *args, **options): + success, errors = do_import() + if options['verbosity'] > 0: + self.stdout.write('Created {} subject creation requests'.format(len(success))) + if options['verbosity'] > 1: + for error in errors: + self.stderr.write(str(error)) + + success, errors = do_cleanup_remote() + if options['verbosity'] > 0: + self.stdout.write( + 'Cleaned up {} subject creation requests from remote service'.format(len(success)) + ) + if options['verbosity'] > 1: + for error in errors: + self.stderr.write(str(error)) -- GitLab