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_to_anonymize = Booking.objects.filter( |
|
31 |
anonymization_datetime__isnull=True, |
|
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={}, |
|
41 |
anonymization_datetime=timezone.now(), |
|
42 |
) |
chrono/agendas/migrations/0061_auto_20200915_1611.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-09-15 14:11 |
|
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', '0060_auto_20200903_1041'), |
|
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 |
migrations.AddField( |
|
32 |
model_name='booking', name='anonymization_datetime', field=models.DateTimeField(null=True), |
|
33 |
), |
|
34 |
] |
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 |
... | ... | |
141 | 141 |
blank=True, |
142 | 142 |
validators=[MaxValueValidator(10000)], |
143 | 143 |
) # eight weeks |
144 |
anonymize_delay = models.PositiveIntegerField( |
|
145 |
_('Anonymize delay (in days)'), |
|
146 |
default=None, |
|
147 |
null=True, |
|
148 |
blank=True, |
|
149 |
validators=[MinValueValidator(30), MaxValueValidator(1000)], |
|
150 |
help_text=_('After this delay, user data contained in bookings will be pruned.'), |
|
151 |
) |
|
144 | 152 |
real_agendas = models.ManyToManyField( |
145 | 153 |
'self', |
146 | 154 |
related_name='virtual_agendas', |
... | ... | |
992 | 1000 |
class Booking(models.Model): |
993 | 1001 |
event = models.ForeignKey(Event, on_delete=models.CASCADE) |
994 | 1002 |
extra_data = JSONField(null=True) |
1003 |
anonymization_datetime = models.DateTimeField(null=True) |
|
995 | 1004 |
cancellation_datetime = models.DateTimeField(null=True) |
996 | 1005 |
in_waiting_list = models.BooleanField(default=False) |
997 | 1006 |
creation_datetime = models.DateTimeField(auto_now_add=True) |
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 | ||
---|---|---|
1238 | 1238 |
# no new email on subsequent run |
1239 | 1239 |
call_command('send_email_notifications') |
1240 | 1240 |
assert len(mailoutbox) == 1 |
1241 | ||
1242 | ||
1243 |
def test_anonymize_bookings(freezer): |
|
1244 |
day = datetime.datetime(year=2020, month=1, day=1) |
|
1245 |
freezer.move_to(day) |
|
1246 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
|
1247 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') |
|
1248 | ||
1249 |
for i in range(5): |
|
1250 |
Booking.objects.create( |
|
1251 |
event=event, |
|
1252 |
extra_data={'test': True}, |
|
1253 |
label='john', |
|
1254 |
user_display_label='john', |
|
1255 |
user_external_id='john', |
|
1256 |
user_name='john', |
|
1257 |
backoffice_url='https://example.org', |
|
1258 |
) |
|
1259 | ||
1260 |
freezer.move_to(day + datetime.timedelta(days=50)) |
|
1261 |
new_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') |
|
1262 |
booking = Booking.objects.create(event=new_event, label='hop') |
|
1263 | ||
1264 |
freezer.move_to(day + datetime.timedelta(days=101)) |
|
1265 |
call_command('anonymize_bookings') |
|
1266 |
assert not Booking.objects.filter(anonymization_datetime__isnull=False).exists() |
|
1267 | ||
1268 |
# now ask for anonymization |
|
1269 |
agenda.anonymize_delay = 100 |
|
1270 |
agenda.save() |
|
1271 | ||
1272 |
call_command('anonymize_bookings') |
|
1273 |
assert ( |
|
1274 |
Booking.objects.filter( |
|
1275 |
label='', |
|
1276 |
user_display_label='', |
|
1277 |
user_external_id='', |
|
1278 |
user_name='', |
|
1279 |
backoffice_url='https://example.org', |
|
1280 |
extra_data={}, |
|
1281 |
anonymization_datetime=now(), |
|
1282 |
).count() |
|
1283 |
== 5 |
|
1284 |
) |
|
1285 | ||
1286 |
booking.refresh_from_db() |
|
1287 |
assert booking.label == 'hop' |
|
1288 |
assert not booking.anonymization_datetime |
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 |
- |