From 91acf03ca17c756d47512eef65effb42e82f6851 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 | 42 ++++++++++++++++ .../migrations/0061_auto_20200915_1611.py | 34 +++++++++++++ chrono/agendas/models.py | 11 ++++- chrono/manager/forms.py | 1 + debian/chrono.cron.daily | 3 ++ tests/test_agendas.py | 48 +++++++++++++++++++ tests/test_manager.py | 3 ++ 7 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 chrono/agendas/management/commands/anonymize_bookings.py create mode 100644 chrono/agendas/migrations/0061_auto_20200915_1611.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..4ae6ee4 --- /dev/null +++ b/chrono/agendas/management/commands/anonymize_bookings.py @@ -0,0 +1,42 @@ +# 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_to_anonymize = Booking.objects.filter( + anonymization_datetime__isnull=True, + 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={}, + anonymization_datetime=timezone.now(), + ) diff --git a/chrono/agendas/migrations/0061_auto_20200915_1611.py b/chrono/agendas/migrations/0061_auto_20200915_1611.py new file mode 100644 index 0000000..c44e5f8 --- /dev/null +++ b/chrono/agendas/migrations/0061_auto_20200915_1611.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-09-15 14:11 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0060_auto_20200903_1041'), + ] + + 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)', + ), + ), + migrations.AddField( + model_name='booking', name='anonymization_datetime', field=models.DateTimeField(null=True), + ), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 558d56e..3907c82 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -32,7 +32,7 @@ from django.contrib.auth.models import Group from django.contrib.postgres.fields import ArrayField 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 @@ -141,6 +141,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', @@ -992,6 +1000,7 @@ class Event(models.Model): class Booking(models.Model): event = models.ForeignKey(Event, on_delete=models.CASCADE) extra_data = JSONField(null=True) + anonymization_datetime = models.DateTimeField(null=True) cancellation_datetime = models.DateTimeField(null=True) in_waiting_list = models.BooleanField(default=False) creation_datetime = models.DateTimeField(auto_now_add=True) diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 7234769..803c913 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -72,6 +72,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 b64eaa2..c22d934 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -1238,3 +1238,51 @@ def test_agenda_notifications_cancelled(mailoutbox): # no new email on subsequent run call_command('send_email_notifications') assert len(mailoutbox) == 1 + + +def test_anonymize_bookings(freezer): + day = datetime.datetime(year=2020, month=1, day=1) + freezer.move_to(day) + 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(day + datetime.timedelta(days=50)) + new_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') + booking = Booking.objects.create(event=new_event, label='hop') + + freezer.move_to(day + datetime.timedelta(days=101)) + call_command('anonymize_bookings') + assert not Booking.objects.filter(anonymization_datetime__isnull=False).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={}, + anonymization_datetime=now(), + ).count() + == 5 + ) + + booking.refresh_from_db() + assert booking.label == 'hop' + assert not booking.anonymization_datetime diff --git a/tests/test_manager.py b/tests/test_manager.py index 3a562b8..85d61f4 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -875,6 +875,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) @@ -882,6 +883,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