0001-agendas-add-anonymize-delay-45288.patch
chrono/agendas/management/commands/anonymize_bookings.py | ||
---|---|---|
1 |
# chrono - agendas system |
|
2 |
# Copyright (C) 2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from datetime import timedelta |
|
18 | ||
19 |
from django.core.management.base import BaseCommand |
|
20 |
from django.db.models import F |
|
21 |
from django.utils import timezone |
|
22 | ||
23 |
from chrono.agendas.models import Booking |
|
24 | ||
25 | ||
26 |
class Command(BaseCommand): |
|
27 |
help = 'Anonymize bookings according to agendas delays' |
|
28 | ||
29 |
def handle(self, **options): |
|
30 |
bookings = Booking.objects.exclude(extra_data={'anonymized': True}) |
|
31 |
bookings_to_anonymize = bookings.filter( |
|
32 |
creation_datetime__lt=timezone.now() - timedelta(days=1) * F('event__agenda__anonymize_delay') |
|
33 |
) |
|
34 | ||
35 |
bookings_to_anonymize.update( |
|
36 |
label='', |
|
37 |
user_display_label='', |
|
38 |
user_external_id='', |
|
39 |
user_name='', |
|
40 |
extra_data={'anonymized': True}, |
|
41 |
) |
chrono/agendas/migrations/0056_agenda_anonymize_delay.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-08-13 12:21 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
import django.core.validators |
|
6 |
from django.db import migrations, models |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
dependencies = [ |
|
12 |
('agendas', '0055_booking_cancel_callback_url'), |
|
13 |
] |
|
14 | ||
15 |
operations = [ |
|
16 |
migrations.AddField( |
|
17 |
model_name='agenda', |
|
18 |
name='anonymize_delay', |
|
19 |
field=models.PositiveIntegerField( |
|
20 |
blank=True, |
|
21 |
default=None, |
|
22 |
help_text='After this delay, user data contained in bookings will be pruned.', |
|
23 |
null=True, |
|
24 |
validators=[ |
|
25 |
django.core.validators.MinValueValidator(30), |
|
26 |
django.core.validators.MaxValueValidator(1000), |
|
27 |
], |
|
28 |
verbose_name='Anonymize delay (in days)', |
|
29 |
), |
|
30 |
), |
|
31 |
] |
chrono/agendas/models.py | ||
---|---|---|
31 | 31 |
from django.contrib.auth.models import Group |
32 | 32 |
from django.core.exceptions import FieldDoesNotExist |
33 | 33 |
from django.core.exceptions import ValidationError |
34 |
from django.core.validators import MaxValueValidator |
|
34 |
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
35 | 35 |
from django.db import models, transaction |
36 | 36 |
from django.db.models import Count, Q, Case, When |
37 | 37 |
from django.urls import reverse |
... | ... | |
124 | 124 |
blank=True, |
125 | 125 |
validators=[MaxValueValidator(10000)], |
126 | 126 |
) # eight weeks |
127 |
anonymize_delay = models.PositiveIntegerField( |
|
128 |
_('Anonymize delay (in days)'), |
|
129 |
default=None, |
|
130 |
null=True, |
|
131 |
blank=True, |
|
132 |
validators=[MinValueValidator(30), MaxValueValidator(1000)], |
|
133 |
help_text=_('After this delay, user data contained in bookings will be pruned.'), |
|
134 |
) |
|
127 | 135 |
real_agendas = models.ManyToManyField( |
128 | 136 |
'self', |
129 | 137 |
related_name='virtual_agendas', |
chrono/manager/forms.py | ||
---|---|---|
71 | 71 |
'view_role', |
72 | 72 |
'minimal_booking_delay', |
73 | 73 |
'maximal_booking_delay', |
74 |
'anonymize_delay', |
|
74 | 75 |
'default_view', |
75 | 76 |
] |
76 | 77 |
debian/chrono.cron.daily | ||
---|---|---|
1 |
#! /bin/sh |
|
2 | ||
3 |
/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command anonymize_bookings --all-tenants |
tests/test_agendas.py | ||
---|---|---|
1008 | 1008 | |
1009 | 1009 |
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists() |
1010 | 1010 |
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists() |
1011 | ||
1012 | ||
1013 |
def test_anonymize_bookings(freezer): |
|
1014 |
freezer.move_to('2020-01-01') |
|
1015 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
|
1016 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') |
|
1017 | ||
1018 |
for i in range(5): |
|
1019 |
Booking.objects.create( |
|
1020 |
event=event, |
|
1021 |
extra_data={'test': True}, |
|
1022 |
label='john', |
|
1023 |
user_display_label='john', |
|
1024 |
user_external_id='john', |
|
1025 |
user_name='john', |
|
1026 |
backoffice_url='https://example.org', |
|
1027 |
) |
|
1028 | ||
1029 |
freezer.move_to('2020-04-01') |
|
1030 |
booking = Booking.objects.create(event=event, label='hop') |
|
1031 | ||
1032 |
freezer.move_to('2020-04-15') # about 100 days after first bookings |
|
1033 |
call_command('anonymize_bookings') |
|
1034 |
assert not Booking.objects.filter(extra_data={'anonymized': True}).exists() |
|
1035 | ||
1036 |
# now ask for anonymization |
|
1037 |
agenda.anonymize_delay = 100 |
|
1038 |
agenda.save() |
|
1039 | ||
1040 |
call_command('anonymize_bookings') |
|
1041 |
assert ( |
|
1042 |
Booking.objects.filter( |
|
1043 |
label='', |
|
1044 |
user_display_label='', |
|
1045 |
user_external_id='', |
|
1046 |
user_name='', |
|
1047 |
backoffice_url='https://example.org', |
|
1048 |
extra_data={'anonymized': True}, |
|
1049 |
).count() |
|
1050 |
== 5 |
|
1051 |
) |
|
1052 | ||
1053 |
booking.refresh_from_db() |
|
1054 |
assert booking.label == 'hop' |
|
1055 |
assert not booking.extra_data |
tests/test_manager.py | ||
---|---|---|
868 | 868 |
resp = app.get('/manage/agendas/%s/edit' % agenda_events.pk) |
869 | 869 |
assert resp.form['label'].value == 'Foo bar' |
870 | 870 |
resp.form['label'] = 'Foo baz' |
871 |
resp.form['anonymize_delay'] = 365 |
|
871 | 872 |
assert 'default_view' in resp.context['form'].fields |
872 | 873 |
resp = resp.form.submit() |
873 | 874 |
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda_events.pk) |
... | ... | |
875 | 876 |
assert 'has_resources' not in resp.context |
876 | 877 |
assert 'Foo baz' in resp.text |
877 | 878 |
assert '<h2>Settings' in resp.text |
879 |
agenda_events.refresh_from_db() |
|
880 |
assert agenda_events.anonymize_delay == 365 |
|
878 | 881 | |
879 | 882 |
resp = app.get('/manage/agendas/%s/edit' % agenda_meetings.pk) |
880 | 883 |
assert 'default_view' not in resp.context['form'].fields |
881 |
- |