From 4b8a1f49b1cb16e019c9ef8234be3ef2d8e849f0 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 13 Aug 2020 11:48:52 +0200 Subject: [PATCH] agendas: add anonymize delay (#45288) --- .../management/commands/anonymize_bookings.py | 41 +++++++++++++++++ .../migrations/0056_agenda_anonymize_delay.py | 31 +++++++++++++ chrono/agendas/models.py | 10 ++++- chrono/manager/forms.py | 1 + debian/chrono.cron.daily | 3 ++ tests/test_agendas.py | 45 +++++++++++++++++++ tests/test_manager.py | 3 ++ 7 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 chrono/agendas/management/commands/anonymize_bookings.py create mode 100644 chrono/agendas/migrations/0056_agenda_anonymize_delay.py create mode 100644 debian/chrono.cron.daily diff --git a/chrono/agendas/management/commands/anonymize_bookings.py b/chrono/agendas/management/commands/anonymize_bookings.py new file mode 100644 index 0000000..937bbe6 --- /dev/null +++ b/chrono/agendas/management/commands/anonymize_bookings.py @@ -0,0 +1,41 @@ +# chrono - agendas system +# Copyright (C) 2020 Entr'ouvert +# +# This program 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. +# +# This program 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 this program. If not, see . + +from datetime import timedelta + +from django.core.management.base import BaseCommand +from django.db.models import F +from django.utils import timezone + +from chrono.agendas.models import Booking + + +class Command(BaseCommand): + help = 'Anonymize bookings according to agendas delays' + + def handle(self, **options): + bookings = Booking.objects.exclude(extra_data={'anonymized': True}) + bookings_to_anonymize = bookings.filter( + creation_datetime__lt=timezone.now() - timedelta(days=1) * F('event__agenda__anonymize_delay') + ) + + bookings_to_anonymize.update( + label='', + user_display_label='', + user_external_id='', + user_name='', + extra_data={'anonymized': True}, + ) diff --git a/chrono/agendas/migrations/0056_agenda_anonymize_delay.py b/chrono/agendas/migrations/0056_agenda_anonymize_delay.py new file mode 100644 index 0000000..a1c2abd --- /dev/null +++ b/chrono/agendas/migrations/0056_agenda_anonymize_delay.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-08-13 12:21 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0055_booking_cancel_callback_url'), + ] + + operations = [ + migrations.AddField( + model_name='agenda', + name='anonymize_delay', + field=models.PositiveIntegerField( + blank=True, + default=None, + help_text='After this delay, user data contained in bookings will be pruned.', + null=True, + validators=[ + django.core.validators.MinValueValidator(30), + django.core.validators.MaxValueValidator(1000), + ], + verbose_name='Anonymize delay (in days)', + ), + ), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 665d8de..77e34a0 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -31,7 +31,7 @@ from django.conf import settings from django.contrib.auth.models import Group from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import ValidationError -from django.core.validators import MaxValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction from django.db.models import Count, Q, Case, When from django.urls import reverse @@ -124,6 +124,14 @@ class Agenda(models.Model): blank=True, validators=[MaxValueValidator(10000)], ) # eight weeks + anonymize_delay = models.PositiveIntegerField( + _('Anonymize delay (in days)'), + default=None, + null=True, + blank=True, + validators=[MinValueValidator(30), MaxValueValidator(1000)], + help_text=_('After this delay, user data contained in bookings will be pruned.'), + ) real_agendas = models.ManyToManyField( 'self', related_name='virtual_agendas', diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 6dbd684..322b035 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -71,6 +71,7 @@ class AgendaEditForm(AgendaAddForm): 'view_role', 'minimal_booking_delay', 'maximal_booking_delay', + 'anonymize_delay', 'default_view', ] diff --git a/debian/chrono.cron.daily b/debian/chrono.cron.daily new file mode 100644 index 0000000..422c2dc --- /dev/null +++ b/debian/chrono.cron.daily @@ -0,0 +1,3 @@ +#! /bin/sh + +/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command anonymize_bookings --all-tenants diff --git a/tests/test_agendas.py b/tests/test_agendas.py index d971e2a..c6cbc2d 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -1008,3 +1008,48 @@ def test_agenda_virtual_duplicate(): assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists() assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists() + + +def test_anonymize_bookings(freezer): + freezer.move_to('2020-01-01') + agenda = Agenda.objects.create(label='Agenda', kind='events') + event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') + + for i in range(5): + Booking.objects.create( + event=event, + extra_data={'test': True}, + label='john', + user_display_label='john', + user_external_id='john', + user_name='john', + backoffice_url='https://example.org', + ) + + freezer.move_to('2020-04-01') + booking = Booking.objects.create(event=event, label='hop') + + freezer.move_to('2020-04-15') # about 100 days after first bookings + call_command('anonymize_bookings') + assert not Booking.objects.filter(extra_data={'anonymized': True}).exists() + + # now ask for anonymization + agenda.anonymize_delay = 100 + agenda.save() + + call_command('anonymize_bookings') + assert ( + Booking.objects.filter( + label='', + user_display_label='', + user_external_id='', + user_name='', + backoffice_url='https://example.org', + extra_data={'anonymized': True}, + ).count() + == 5 + ) + + booking.refresh_from_db() + assert booking.label == 'hop' + assert not booking.extra_data diff --git a/tests/test_manager.py b/tests/test_manager.py index d75cac0..adeb546 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -868,6 +868,7 @@ def test_options_agenda(app, admin_user): resp = app.get('/manage/agendas/%s/edit' % agenda_events.pk) assert resp.form['label'].value == 'Foo bar' resp.form['label'] = 'Foo baz' + resp.form['anonymize_delay'] = 365 assert 'default_view' in resp.context['form'].fields resp = resp.form.submit() assert resp.location.endswith('/manage/agendas/%s/settings' % agenda_events.pk) @@ -875,6 +876,8 @@ def test_options_agenda(app, admin_user): assert 'has_resources' not in resp.context assert 'Foo baz' in resp.text assert '

Settings' in resp.text + agenda_events.refresh_from_db() + assert agenda_events.anonymize_delay == 365 resp = app.get('/manage/agendas/%s/edit' % agenda_meetings.pk) assert 'default_view' not in resp.context['form'].fields -- 2.20.1