Commit 5b179b40 authored by Luko van der Maas's avatar Luko van der Maas
Browse files

Merge branch 'feature/manual-data-minimisation' into 'master'

Add manual data minimisation functionality

See merge request !1189
parents ef676998 4a5d8b29
...@@ -3,7 +3,7 @@ This module registers admin pages for the models ...@@ -3,7 +3,7 @@ This module registers admin pages for the models
""" """
import csv import csv
import datetime import datetime
from django.contrib import admin from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
...@@ -11,7 +11,8 @@ from django.http import HttpResponse ...@@ -11,7 +11,8 @@ from django.http import HttpResponse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from members.models import EmailChange from members import services
from members.models import EmailChange, Member
from . import forms, models from . import forms, models
...@@ -93,7 +94,7 @@ class UserAdmin(BaseUserAdmin): ...@@ -93,7 +94,7 @@ class UserAdmin(BaseUserAdmin):
add_form = forms.UserCreationForm add_form = forms.UserCreationForm
actions = ['address_csv_export', 'student_number_csv_export', actions = ['address_csv_export', 'student_number_csv_export',
'email_csv_export'] 'email_csv_export', 'minimise_data']
inlines = (ProfileInline, MembershipInline,) inlines = (ProfileInline, MembershipInline,)
list_filter = (MembershipTypeListFilter, list_filter = (MembershipTypeListFilter,
...@@ -161,6 +162,25 @@ class UserAdmin(BaseUserAdmin): ...@@ -161,6 +162,25 @@ class UserAdmin(BaseUserAdmin):
student_number_csv_export.short_description = _('Download student number ' student_number_csv_export.short_description = _('Download student number '
'label for selected users') 'label for selected users')
def minimise_data(self, request, queryset):
processed = len(services.execute_data_minimisation(
members=Member.objects.filter(pk__in=queryset)))
if processed == 0:
self.message_user(
request,
_('Data minimisation could not be executed '
'for the selected user(s).'),
messages.ERROR
)
else:
self.message_user(
request,
_('Data minimisation was executed '
'for {} user(s).').format(processed),
messages.SUCCESS
)
minimise_data.short_description = _('Minimise data for the selected users')
@admin.register(models.Member) @admin.register(models.Member)
class MemberAdmin(UserAdmin): class MemberAdmin(UserAdmin):
......
from datetime import date from datetime import date
from django.db.models import Q from django.db.models import Q, Count
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext from django.utils.translation import gettext
...@@ -164,17 +164,21 @@ def process_email_change(change_request): ...@@ -164,17 +164,21 @@ def process_email_change(change_request):
emails.send_email_change_completion_message(change_request) emails.send_email_change_completion_message(change_request)
def execute_data_minimisation(dry_run=False): def execute_data_minimisation(dry_run=False, members=None):
""" """
Clean the profiles of members/users of whom the last membership ended Clean the profiles of members/users of whom the last membership ended
at least 31 days ago at least 31 days ago
:param dry_run: does not really remove data if True :param dry_run: does not really remove data if True
:param members: queryset of members to process, optional
:return: list of processed members :return: list of processed members
""" """
members = (Member.objects if not members:
.exclude(Q(membership__until__isnull=True) | members = Member.objects
Q(membership__until__gt=timezone.now().date())) members = (members.annotate(membership_count=Count('membership'))
.exclude((Q(membership__until__isnull=True) |
Q(membership__until__gt=timezone.now().date())) &
Q(membership_count__gt=0))
.distinct() .distinct()
.prefetch_related('membership_set', 'profile')) .prefetch_related('membership_set', 'profile'))
deletion_period = timezone.now().date() - timezone.timedelta(days=31) deletion_period = timezone.now().date() - timezone.timedelta(days=31)
......
...@@ -224,77 +224,107 @@ class DataMinimisationTest(TestCase): ...@@ -224,77 +224,107 @@ class DataMinimisationTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.member = Member.objects.create( cls.m1 = Member.objects.create(
username='test1', username='test1',
first_name='Test1', first_name='Test1',
last_name='Example', last_name='Example',
email='test1@example.org' email='test1@example.org'
) )
Profile.objects.create( Profile.objects.create(
user=cls.member, user=cls.m1,
language='nl', language='nl',
student_number='s1234567' student_number='s1234567'
) )
cls.membership = Membership.objects.create( cls.s1 = Membership.objects.create(
user=cls.member, user=cls.m1,
type=Membership.MEMBER,
since=timezone.now().replace(year=2017, month=9, day=1),
until=timezone.now().replace(year=2018, month=9, day=1)
)
cls.m2 = Member.objects.create(
username='test2',
first_name='Test2',
last_name='Example',
email='test2@example.org'
)
Profile.objects.create(
user=cls.m2,
language='nl',
student_number='s7654321'
)
cls.s2 = Membership.objects.create(
user=cls.m2,
type=Membership.MEMBER, type=Membership.MEMBER,
since=timezone.now().replace(year=2017, month=9, day=1), since=timezone.now().replace(year=2017, month=9, day=1),
until=timezone.now().replace(year=2018, month=9, day=1) until=timezone.now().replace(year=2018, month=9, day=1)
) )
def test_removes_after_31_days(self): def test_removes_after_31_days_or_no_membership(self):
processed = services.execute_data_minimisation(True) with self.subTest('Deletes after 31 days'):
self.assertEqual(len(processed), 1) processed = services.execute_data_minimisation(True)
self.assertEqual(processed[0], self.member) self.assertEqual(len(processed), 2)
self.assertEqual(processed[0], self.m1)
self.membership.until = timezone.now().replace( with self.subTest('Deletes after 31 days'):
year=2018, month=11, day=1) self.s1.until = timezone.now().replace(
self.membership.save() year=2018, month=11, day=1)
processed = services.execute_data_minimisation(True) self.s1.save()
self.assertEqual(len(processed), 0) processed = services.execute_data_minimisation(True)
self.assertEqual(len(processed), 1)
with self.subTest('Deletes when no memberships'):
self.s1.delete()
processed = services.execute_data_minimisation(True)
self.assertEqual(len(processed), 2)
def test_dry_run(self): def test_dry_run(self):
with self.subTest('With dry_run=True'): with self.subTest('With dry_run=True'):
services.execute_data_minimisation(True) services.execute_data_minimisation(True)
self.member.refresh_from_db() self.m1.refresh_from_db()
self.assertEqual(self.member.profile.student_number, 's1234567') self.assertEqual(self.m1.profile.student_number, 's1234567')
with self.subTest('With dry_run=False'): with self.subTest('With dry_run=False'):
services.execute_data_minimisation(False) services.execute_data_minimisation(False)
self.member.refresh_from_db() self.m1.refresh_from_db()
self.assertIsNone(self.member.profile.student_number) self.assertIsNone(self.m1.profile.student_number)
def test_provided_queryset(self):
processed = services.execute_data_minimisation(True,
members=Member.objects)
self.assertEqual(len(processed), 2)
self.assertEqual(processed[0], self.m1)
def test_does_not_affect_current_members(self): def test_does_not_affect_current_members(self):
with self.subTest('Membership ends in future'): with self.subTest('Membership ends in future'):
self.membership.until = timezone.now().replace( self.s1.until = timezone.now().replace(
year=2019, month=9, day=1) year=2019, month=9, day=1)
self.membership.save() self.s1.save()
processed = services.execute_data_minimisation(True) processed = services.execute_data_minimisation(True)
self.assertEqual(len(processed), 0) self.assertEqual(len(processed), 1)
with self.subTest('Never ending membership'): with self.subTest('Never ending membership'):
self.membership.until = None self.s1.until = None
self.membership.save() self.s1.save()
processed = services.execute_data_minimisation(True) processed = services.execute_data_minimisation(True)
self.assertEqual(len(processed), 0) self.assertEqual(len(processed), 1)
self.membership.until = timezone.now().replace( self.s1.until = timezone.now().replace(
year=2018, month=9, day=1) year=2018, month=9, day=1)
self.membership.save() self.s1.save()
with self.subTest('Newer year membership after expired one'): with self.subTest('Newer year membership after expired one'):
m = Membership.objects.create( m = Membership.objects.create(
user=self.member, user=self.m1,
type=Membership.MEMBER, type=Membership.MEMBER,
since=timezone.now().replace(year=2018, month=9, day=10), since=timezone.now().replace(year=2018, month=9, day=10),
until=timezone.now().replace(year=2019, month=8, day=31), until=timezone.now().replace(year=2019, month=8, day=31),
) )
processed = services.execute_data_minimisation(True) processed = services.execute_data_minimisation(True)
self.assertEqual(len(processed), 0) self.assertEqual(len(processed), 1)
m.delete() m.delete()
with self.subTest('Newer study membership after expired one'): with self.subTest('Newer study membership after expired one'):
m = Membership.objects.create( m = Membership.objects.create(
user=self.member, user=self.m1,
type=Membership.MEMBER, type=Membership.MEMBER,
since=timezone.now().replace(year=2018, month=9, day=10), since=timezone.now().replace(year=2018, month=9, day=10),
until=None until=None
) )
processed = services.execute_data_minimisation(True) processed = services.execute_data_minimisation(True)
self.assertEqual(len(processed), 0) self.assertEqual(len(processed), 1)
m.delete() m.delete()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment