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/0062_agenda_anonymize_delay.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-09-07 12:02 |
|
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', '0061_auto_20200907_1401'), |
|
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 | ||
---|---|---|
32 | 32 |
from django.contrib.postgres.fields import ArrayField |
33 | 33 |
from django.core.exceptions import FieldDoesNotExist |
34 | 34 |
from django.core.exceptions import ValidationError |
35 |
from django.core.validators import MaxValueValidator |
|
35 |
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
36 | 36 |
from django.db import models, transaction |
37 | 37 |
from django.db.models import Count, Q, Case, When |
38 | 38 |
from django.urls import reverse |
... | ... | |
126 | 126 |
blank=True, |
127 | 127 |
validators=[MaxValueValidator(10000)], |
128 | 128 |
) # eight weeks |
129 |
anonymize_delay = models.PositiveIntegerField( |
|
130 |
_('Anonymize delay (in days)'), |
|
131 |
default=None, |
|
132 |
null=True, |
|
133 |
blank=True, |
|
134 |
validators=[MinValueValidator(30), MaxValueValidator(1000)], |
|
135 |
help_text=_('After this delay, user data contained in bookings will be pruned.'), |
|
136 |
) |
|
129 | 137 |
real_agendas = models.ManyToManyField( |
130 | 138 |
'self', |
131 | 139 |
related_name='virtual_agendas', |
chrono/manager/forms.py | ||
---|---|---|
72 | 72 |
'view_role', |
73 | 73 |
'minimal_booking_delay', |
74 | 74 |
'maximal_booking_delay', |
75 |
'anonymize_delay', |
|
75 | 76 |
'default_view', |
76 | 77 |
] |
77 | 78 |
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 | ||
---|---|---|
1236 | 1236 |
# no new email on subsequent run |
1237 | 1237 |
call_command('send_email_notifications') |
1238 | 1238 |
assert len(mailoutbox) == 1 |
1239 | ||
1240 | ||
1241 |
def test_anonymize_bookings(freezer): |
|
1242 |
freezer.move_to('2020-01-01') |
|
1243 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
|
1244 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') |
|
1245 | ||
1246 |
for i in range(5): |
|
1247 |
Booking.objects.create( |
|
1248 |
event=event, |
|
1249 |
extra_data={'test': True}, |
|
1250 |
label='john', |
|
1251 |
user_display_label='john', |
|
1252 |
user_external_id='john', |
|
1253 |
user_name='john', |
|
1254 |
backoffice_url='https://example.org', |
|
1255 |
) |
|
1256 | ||
1257 |
freezer.move_to('2020-04-01') |
|
1258 |
booking = Booking.objects.create(event=event, label='hop') |
|
1259 | ||
1260 |
freezer.move_to('2020-04-15') # about 100 days after first bookings |
|
1261 |
call_command('anonymize_bookings') |
|
1262 |
assert not Booking.objects.filter(extra_data={'anonymized': True}).exists() |
|
1263 | ||
1264 |
# now ask for anonymization |
|
1265 |
agenda.anonymize_delay = 100 |
|
1266 |
agenda.save() |
|
1267 | ||
1268 |
call_command('anonymize_bookings') |
|
1269 |
assert ( |
|
1270 |
Booking.objects.filter( |
|
1271 |
label='', |
|
1272 |
user_display_label='', |
|
1273 |
user_external_id='', |
|
1274 |
user_name='', |
|
1275 |
backoffice_url='https://example.org', |
|
1276 |
extra_data={'anonymized': True}, |
|
1277 |
).count() |
|
1278 |
== 5 |
|
1279 |
) |
|
1280 | ||
1281 |
booking.refresh_from_db() |
|
1282 |
assert booking.label == 'hop' |
|
1283 |
assert not booking.extra_data |
tests/test_manager.py | ||
---|---|---|
875 | 875 |
resp = app.get('/manage/agendas/%s/edit' % agenda_events.pk) |
876 | 876 |
assert resp.form['label'].value == 'Foo bar' |
877 | 877 |
resp.form['label'] = 'Foo baz' |
878 |
resp.form['anonymize_delay'] = 365 |
|
878 | 879 |
assert 'default_view' in resp.context['form'].fields |
879 | 880 |
resp = resp.form.submit() |
880 | 881 |
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda_events.pk) |
... | ... | |
882 | 883 |
assert 'has_resources' not in resp.context |
883 | 884 |
assert 'Foo baz' in resp.text |
884 | 885 |
assert '<h2>Settings' in resp.text |
886 |
agenda_events.refresh_from_db() |
|
887 |
assert agenda_events.anonymize_delay == 365 |
|
885 | 888 | |
886 | 889 |
resp = app.get('/manage/agendas/%s/edit' % agenda_meetings.pk) |
887 | 890 |
assert 'default_view' not in resp.context['form'].fields |
888 |
- |