from django.conf import settings
import urllib
+import datetime
from postgresqleu.util.middleware import RedirectException
+from postgresqleu.util.db import exec_to_dict, exec_no_result
from models import Conference, ConferenceRegistration
from models import RegistrationType, RegistrationClass
'Sponsor invoices': Invoice.objects.filter(paidat__isnull=True, sponsor__conference=conference),
},
})
+
+
+@transaction.atomic
+def purge_personal_data(request, urlname):
+ conference = get_authenticated_conference(request, urlname)
+
+ if conference.personal_data_purged:
+ messages.warning(request, 'Personal data for this conference has already been purged')
+ return HttpResponseRedirect('../')
+
+ if request.method == 'POST':
+ exec_no_result("INSERT INTO confreg_aggregatedtshirtsizes (conference_id, size_id, num) SELECT conference_id, shirtsize_id, count(*) FROM confreg_conferenceregistration WHERE conference_id=%(confid)s AND shirtsize_id IS NOT NULL GROUP BY conference_id, shirtsize_id", {'confid': conference.id, })
+ exec_no_result("INSERT INTO confreg_aggregateddietary (conference_id, dietary, num) SELECT conference_id, lower(dietary), count(*) FROM confreg_conferenceregistration WHERE conference_id=%(confid)s AND dietary IS NOT NULL AND dietary != '' GROUP BY conference_id, lower(dietary)", {'confid': conference.id, })
+ exec_no_result("UPDATE confreg_conferenceregistration SET shirtsize_id=NULL, dietary='', phone='', address='' WHERE conference_id=%(confid)s", {'confid': conference.id, })
+ conference.personal_data_purged = datetime.datetime.now()
+ conference.save()
+ messages.info(request, "Personal data purged from conference")
+ return HttpResponseRedirect('../')
+
+ return render(request, 'confreg/admin_purge_personal_data.html', {
+ 'conference': conference,
+ 'counts': exec_to_dict("""SELECT
+ count(1) FILTER (WHERE shirtsize_id IS NOT NULL) AS "T-shirt size registrations",
+ count(1) FILTER (WHERE dietary IS NOT NULL AND dietary != '') AS "Dietary needs",
+ count(1) FILTER (WHERE phone IS NOT NULL AND phone != '') AS "Phone numbers",
+ count(1) FILTER (WHERE address IS NOT NULL AND address != '') AS "Addresses"
+FROM confreg_conferenceregistration WHERE conference_id=%(confid)s""", {
+ 'confid': conference.id,
+ })[0],
+ })
--- /dev/null
+#
+# Send warnings when a conference has not purged their personal
+# data long after the conference ended.
+#
+# Intended to run on a weekly basis or so, as it will keep repeating
+# the reminders every time.
+#
+from django.core.management.base import BaseCommand, CommandError
+from django.db import transaction
+from django.conf import settings
+
+from datetime import datetime, timedelta
+
+from postgresqleu.mailqueue.util import send_simple_mail
+
+from postgresqleu.confreg.models import Conference
+
+class Command(BaseCommand):
+ help = 'Send warnings about purging personal data'
+
+ def handle(self, *args, **options):
+ for conference in Conference.objects.filter(personal_data_purged__isnull=True,
+ enddate__lt=datetime.now() - timedelta(days=30)) \
+ .extra(where=["EXISTS (SELECT 1 FROM confreg_conferenceregistration r WHERE r.conference_id=confreg_conference.id)"]):
+ send_simple_mail(conference.contactaddr,
+ conference.contactaddr,
+ "{0}: time to purge personal data?".format(conference.conferencename),
+ """Conference {0} finished on {1},
+but personal data has not been purged.
+
+In accordance with the rules, personal data should be purged
+as soon as it's no longer needed. So please consider doing so,
+from the conference dashboard:
+
+{2}/events/admin/{3}/
+""".format(conference.conferencename, conference.enddate, settings.SITEBASE, conference.urlname),
+ sendername = conference.conferencename,
+ receivername = conference.conferencename,
+ bcc = settings.ADMINS[0][1],
+ )
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('confreg', '0018_constraint_reg_payments'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AggregatedDietary',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('dietary', models.CharField(max_length=100)),
+ ('num', models.IntegerField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='AggregatedTshirtSizes',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('num', models.IntegerField()),
+ ],
+ ),
+ migrations.AddField(
+ model_name='conference',
+ name='personal_data_purged',
+ field=models.DateTimeField(help_text=b'Personal data for registrations for this conference have been purged', null=True, blank=True),
+ ),
+ migrations.AddField(
+ model_name='aggregatedtshirtsizes',
+ name='conference',
+ field=models.ForeignKey(to='confreg.Conference'),
+ ),
+ migrations.AddField(
+ model_name='aggregatedtshirtsizes',
+ name='size',
+ field=models.ForeignKey(to='confreg.ShirtSize'),
+ ),
+ migrations.AddField(
+ model_name='aggregateddietary',
+ name='conference',
+ field=models.ForeignKey(to='confreg.Conference'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='aggregatedtshirtsizes',
+ unique_together=set([('conference', 'size')]),
+ ),
+ migrations.AlterUniqueTogether(
+ name='aggregateddietary',
+ unique_together=set([('conference', 'dietary')]),
+ ),
+ ]
invoice_autocancel_hours = models.IntegerField(blank=True, null=True, validators=[MinValueValidator(1),], verbose_name="Autocancel invoices", help_text="Automatically cancel invoices after this many hours")
attendees_before_waitlist = models.IntegerField(blank=False, null=False, default=0, validators=[MinValueValidator(0),], verbose_name="Attendees before waitlist", help_text="Maximum number of attendees before enabling waitlist management. 0 for no waitlist management")
series = models.ForeignKey(ConferenceSeries, null=False, blank=False)
+ personal_data_purged = models.DateTimeField(null=True, blank=True, help_text="Personal data for registrations for this conference have been purged")
# Attributes that are safe to access in jinja templates
_safe_attributes = ('active', 'askfood', 'askshareemail', 'asktshirt',
return False
+ @property
+ def needs_data_purge(self):
+ return self.enddate < datetime.date.today() and not self.personal_data_purged
+
def clean(self):
cc = super(Conference, self).clean()
if self.sendwelcomemail and not self.welcomemail:
def __unicode__(self):
return u"%s" % (self.reg, )
+
+
+
+class AggregatedTshirtSizes(models.Model):
+ conference = models.ForeignKey(Conference, null=False, blank=False)
+ size = models.ForeignKey(ShirtSize, null=False, blank=False)
+ num = models.IntegerField(null=False, blank=False)
+
+ class Meta:
+ unique_together = ( ('conference', 'size'), )
+
+class AggregatedDietary(models.Model):
+ conference = models.ForeignKey(Conference, null=False, blank=False)
+ dietary = models.CharField(max_length=100, null=False, blank=False)
+ num = models.IntegerField(null=False, blank=False)
+
+ class Meta:
+ unique_together = ( ('conference', 'dietary'), )
url(r'^events/admin/(\w+)/feedbackquestions/(.*/)?$', postgresqleu.confreg.backendviews.edit_feedbackquestions),
url(r'^events/admin/(\w+)/discountcodes/(.*/)?$', postgresqleu.confreg.backendviews.edit_discountcodes),
url(r'^events/admin/(\w+)/pendinginvoices/$', postgresqleu.confreg.backendviews.pendinginvoices),
+ url(r'^events/admin/(\w+)/purgedata/$', postgresqleu.confreg.backendviews.purge_personal_data),
url(r'^events/sponsor/', include('postgresqleu.confsponsor.urls')),
<span class="label label-default">{{c.startdate|date:"Y-m-d"}}</span> - <span class="label label-default">{{c.enddate|date:"Y-m-d"}}</span>
</div>
+{%if c.needs_data_purge %}
+<h2>Purge personal data</h2>
+<p>
+ Once we don't need it anymore, personal data should be purged/aggregated for the conference.
+</p>
+<div class="row">
+ <div class="col-md-3 col-sm-6 col-xs-12 buttonrow"><a class="btn btn-danger btn-block" href="/events/admin/{{c.urlname}}/purgedata/">Purge personal data</a></div>
+</div>
+{%endif%}
+
<h2>Registrations</h2>
<div class="row">
<div class="col-md-3 col-sm-6 col-xs-12 buttonrow"><a class="btn btn-default btn-block" href="/events/admin/{{c.urlname}}/regdashboard/">Registration dashboard</a></div>
--- /dev/null
+{%extends "confreg/confadmin_base.html"%}
+{%block title%}Purge personal data{%endblock%}
+
+{%block layoutblock%}
+<h1>Purge personal data</h1>
+<p>
+ You are about to purge the following data:
+</p>
+<div class="row"><div class="col-sm-6">
+<table class="table">
+{%for k,v in counts.items%}
+ <tr>
+ <th scope="col">{{k}}</th>
+ <td>{{v}}</td>
+ </tr>
+{%endfor%}
+</table>
+</div></div>
+
+<form class="form-horizontal" method="POST" action=".">{%csrf_token%}
+ <input type="submit" class="btn btn-default" value="Purge and summarize data">
+</form>
+
+{%endblock%}