From e1b8fdb3abe6fd1b037874f7554c28cf2b9312e0 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Thu, 26 Apr 2018 12:16:20 +0000 Subject: [PATCH 01/29] Added a page with GDPR-related information on handling of personal information within the datatracker. - Legacy-Id: 15090 --- ietf/help/urls.py | 4 + ietf/templates/base/menu_user.html | 1 + ietf/templates/help/personal-information.html | 83 +++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 ietf/templates/help/personal-information.html diff --git a/ietf/help/urls.py b/ietf/help/urls.py index 26b65528d..9c0bfb157 100644 --- a/ietf/help/urls.py +++ b/ietf/help/urls.py @@ -1,3 +1,6 @@ +# Copyright The IETF Trust 2013-2018, All Rights Reserved + +from django.views.generic import TemplateView from ietf.help import views from ietf.utils.urls import url @@ -6,5 +9,6 @@ urlpatterns = [ url(r'^state/(?P<doc>[-\w]+)/(?P<type>[-\w]+)/?$', views.state), url(r'^state/(?P<doc>[-\w]+)/?$', views.state), url(r'^state/?$', views.state_index), + url(r'^personal-information/?$', TemplateView.as_view(template_name='help/personal-information.html'), name='personal-information'), ] diff --git a/ietf/templates/base/menu_user.html b/ietf/templates/base/menu_user.html index 10fbcd5e1..cde9e31cd 100644 --- a/ietf/templates/base/menu_user.html +++ b/ietf/templates/base/menu_user.html @@ -26,6 +26,7 @@ <li><a rel="nofollow" href="/accounts/reset/">Password reset</a></li> <li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li> {% endif %} + <li><a href="{% url 'personal-information' %}">Handling of personal information</a></li> {% endif %} {% if not request.user.is_authenticated %} diff --git a/ietf/templates/help/personal-information.html b/ietf/templates/help/personal-information.html new file mode 100644 index 000000000..582b59124 --- /dev/null +++ b/ietf/templates/help/personal-information.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2018-2018, All Rights Reserved #} +{% load origin %} + +{% block title %}Personal Information in the Datatracker{% endblock %} + +{% block content %} + {% origin %} +<div class="col-sm-12" style="max-width: 85ex;"> + <h1>Personal Information in the Datatracker</h1> + <p> + + <a href="https://tools.ietf.org/html/rfc3935">RFC 3935, "A Mission Statement for the IETF"</a> lays out + the goal and the mission of the IETF as follows: + </p> + + <samp class="preformatted small"> The goal of the IETF is to make the Internet work better. + + The mission of the IETF is to produce high quality, relevant + technical and engineering documents that influence the way people + design, use, and manage the Internet in such a way as to make the + Internet work better. These documents include protocol standards, + best current practices, and informational documents of various kinds. + </samp> + + <p> + + In order to fulfil its mission, the IETF provides ways to distribute + drafts, discuss them, ballot them, and otherwise process them to the + point where they are considered ready for publication. + + </p> + <p> + + This makes the information in the draft documents, as well as + contributions related to the draft documents and their processing, as + laid out in the "<a href="https://www.ietf.org/about/note-well/">Note + Well</a>" statement, of legitimate interest to the IETF when it pursues + its mission; not only in general terms, but specifically under Article + 6(1) f) of <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1"> + EU's General Data Protection Regulation </a>. + + </p> + <p> + + The datatracker treats all personal information derived from draft documents and + documents published as RFC accordingly, as well as personal information derived from + processing draft documents through the IETF process. This includes author names, + email addresses and other address information provided in draft documents or as + contact information for IETF roles such as Working Group chairs, secretaries, + Area Directors and other roles. + + </p> + <p> + + There is however personal information held in the datatracker which + is not considered covered by Legitimate Interest. This information + requires Consent for its storage and processing, and the person it + relates to may at any time request its removal. This includes: + + </p> + + <ul> + <li>Personal photo</li> + <li>Personal biography</li> + <li>Personal email addresses not derived from IETF contributions</li> + <li>Personal account login information</li> + </ul> + + <p> + + Most of this information can be edited on their <a + href="/accounts/profile/">Account Info</a> page by anybody with an + account. If the datatracker holds such information about a person, and + they don't have an account, a request to the <a + href="mailto:ietf-action@ietf.org">IETF secretariat</a> to change or + remove the information will be honoured. + + </p> + + +</div> +{% endblock %} From e177f45f1a965c7ad1382f6ac74acb0b159db7b2 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Fri, 27 Apr 2018 12:05:05 +0000 Subject: [PATCH 02/29] Updated the personal information page with reviewed text from legal counsel. Fixes issue #2503. - Legacy-Id: 15094 --- ietf/templates/help/personal-information.html | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/ietf/templates/help/personal-information.html b/ietf/templates/help/personal-information.html index 582b59124..32f1d00f2 100644 --- a/ietf/templates/help/personal-information.html +++ b/ietf/templates/help/personal-information.html @@ -14,38 +14,48 @@ the goal and the mission of the IETF as follows: </p> - <samp class="preformatted small"> The goal of the IETF is to make the Internet work better. + <samp class="preformatted small"> The goal of the IETF is to make the Internet work better. The mission of the IETF is to produce high quality, relevant technical and engineering documents that influence the way people design, use, and manage the Internet in such a way as to make the Internet work better. These documents include protocol standards, best current practices, and informational documents of various kinds. - </samp> + </samp> + + <!-- *** NOTE *** + + The following text has been reviewed by legal counsel (Thomas Zych) + on Thu, 26 Apr 2018 17:24:03 -0400 + + *** DO NOT CHANGE WITHOUT LEGAL REVIEW *** + --> <p> - In order to fulfil its mission, the IETF provides ways to distribute - drafts, discuss them, ballot them, and otherwise process them to the - point where they are considered ready for publication. + In order to fulfil its mission, the IETF provides various ways to distribute + and discuss Internet-Drafts, and otherwise process them until there is + consensus that they are ready for publication. </p> <p> - This makes the information in the draft documents, as well as - contributions related to the draft documents and their processing, as + This makes the information in the content of the draft documents, as well + as contributions related to the draft documents and their processing as laid out in the "<a href="https://www.ietf.org/about/note-well/">Note Well</a>" statement, of legitimate interest to the IETF when it pursues its mission; not only in general terms, but specifically under Article - 6(1) f) of <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1"> + 6(1) f) of + <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1"> EU's General Data Protection Regulation </a>. </p> <p> The datatracker treats all personal information derived from draft documents and - documents published as RFC accordingly, as well as personal information derived from - processing draft documents through the IETF process. This includes author names, + documents published as RFC, as well as personal information derived from + processing draft documents through the IETF procedures, in accordance with + applicable law, including the GDPR. This includes author names, email addresses and other address information provided in draft documents or as contact information for IETF roles such as Working Group chairs, secretaries, Area Directors and other roles. @@ -53,28 +63,30 @@ </p> <p> - There is however personal information held in the datatracker which - is not considered covered by Legitimate Interest. This information - requires Consent for its storage and processing, and the person it - relates to may at any time request its removal. This includes: + There is however additional personal information held in the datatracker that + is used for other purposes. This information requires the consent of the + individuals whose information is stored and processed which IETF secures + through various means, and the person it relates to may at any time request + its correction or removal. This includes: </p> <ul> - <li>Personal photo</li> - <li>Personal biography</li> - <li>Personal email addresses not derived from IETF contributions</li> + <li>Personal photograph or other likeness;</li> + <li>Personal biography;</li> + <li>Personal email addresses not derived from IETF contributions; and </li> <li>Personal account login information</li> </ul> <p> - Most of this information can be edited on their <a - href="/accounts/profile/">Account Info</a> page by anybody with an + Most of this information can be edited on the individual's + <a href="/accounts/profile/">Account Info</a> page by anybody with an account. If the datatracker holds such information about a person, and they don't have an account, a request to the <a href="mailto:ietf-action@ietf.org">IETF secretariat</a> to change or - remove the information will be honoured. + remove the information will be honoured to the extent feasible and legally + permitted. </p> From 53c4ac36db08a5b4e5a5eb268aebcc1328395f78 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Fri, 27 Apr 2018 14:00:33 +0000 Subject: [PATCH 03/29] Removed the Person.address field, which is not being used. This was a legacy from the 2001 perl-based datatracker tables. Fixes issue #2504. - Legacy-Id: 15095 --- ietf/doc/tests_draft.py | 2 +- ietf/ietfauth/tests.py | 1 - ietf/nomcom/tests.py | 2 +- ietf/nomcom/utils.py | 2 +- .../migrations/0003_auto_20180426_0517.py | 23 +++++++++++++++++++ ietf/person/models.py | 1 - ietf/person/utils.py | 2 +- ietf/secr/rolodex/tests.py | 2 -- ietf/utils/test_data.py | 2 +- 9 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 ietf/person/migrations/0003_auto_20180426_0517.py diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 9071cc0e8..3c94e4723 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -1435,7 +1435,7 @@ class ChangeReplacesTests(TestCase): expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), group=mars_wg, ) - p = Person.objects.create(address="basea_author") + p = Person.objects.create(name="basea_author") e = Email.objects.create(address="basea_author@example.com", person=p) self.basea.documentauthor_set.create(person=p, email=e, order=1) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index fba73e529..270202d8f 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -207,7 +207,6 @@ class IetfAuthTests(TestCase): "name": u"Test Nãme", "ascii": u"Test Name", "ascii_short": u"T. Name", - "address": "Test address", "affiliation": "Test Org", "active_emails": email_address, } diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 6a710f437..1720a8209 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -694,7 +694,7 @@ class NomcomViewsTest(TestCase): # check objects email = Email.objects.get(address=candidate_email) - Person.objects.get(name=candidate_name, address=candidate_email) + Person.objects.get(name=candidate_name) nominee = Nominee.objects.get(email=email) NomineePosition.objects.get(position=position, nominee=nominee) feedback = Feedback.objects.filter(positions__in=[position], diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 54ff1ec69..4d999da9b 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -375,7 +375,7 @@ def make_nomineeposition_for_newperson(nomcom, candidate_name, candidate_email, email = Email.objects.create(address=candidate_email) person = Person.objects.create(name=candidate_name, ascii=unidecode_name(candidate_name), - address=candidate_email) + ) email.person = person email.save() diff --git a/ietf/person/migrations/0003_auto_20180426_0517.py b/ietf/person/migrations/0003_auto_20180426_0517.py new file mode 100644 index 000000000..0cad90d03 --- /dev/null +++ b/ietf/person/migrations/0003_auto_20180426_0517.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-26 05:17 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0002_auto_20180330_0808'), + ] + + operations = [ + migrations.RemoveField( + model_name='person', + name='address', + ), + migrations.RemoveField( + model_name='personhistory', + name='address', + ), + ] diff --git a/ietf/person/models.py b/ietf/person/models.py index 886802b83..36a51d00e 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -37,7 +37,6 @@ class PersonInfo(models.Model): # The short ascii-form of the name. Also in alias table if non-null ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True, help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).") affiliation = models.CharField(max_length=255, blank=True, help_text="Employer, university, sponsor, etc.") - address = models.TextField(max_length=255, blank=True, help_text="Postal mailing address.") biography = models.TextField(blank=True, help_text="Short biography for use on leadership pages. Use plain text or reStructuredText markup.") photo = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) photo_thumb = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) diff --git a/ietf/person/utils.py b/ietf/person/utils.py index 5909eacf7..ab8382322 100755 --- a/ietf/person/utils.py +++ b/ietf/person/utils.py @@ -34,7 +34,7 @@ def merge_persons(source, target, file=sys.stdout, verbose=False): dedupe_aliases(target) # copy other attributes - for field in ('ascii','ascii_short','address','affiliation'): + for field in ('ascii','ascii_short','affiliation'): if getattr(source,field) and not getattr(target,field): setattr(target,field,getattr(source,field)) target.save() diff --git a/ietf/secr/rolodex/tests.py b/ietf/secr/rolodex/tests.py index 468526dd3..9616e8f66 100644 --- a/ietf/secr/rolodex/tests.py +++ b/ietf/secr/rolodex/tests.py @@ -41,7 +41,6 @@ class RolodexTestCase(TestCase): 'ascii': 'Joe Smith', 'ascii_short': 'Joe S', 'affiliation': 'IETF', - 'address': '100 First Ave', 'email': 'joes@exanple.com', 'submit': 'Submit', } @@ -63,7 +62,6 @@ class RolodexTestCase(TestCase): 'ascii': person.ascii, 'ascii_short': person.ascii_short, 'affiliation': person.affiliation, - 'address': person.address, 'user': user.username, 'email-0-person':person.pk, 'email-0-address': person.email_address(), diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 2d912602d..a8a56a8d4 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -60,7 +60,7 @@ def make_immutable_base_data(): date4 = TelechatDate.objects.create(date=t + datetime.timedelta(days=14 * 3)).date # pyflakes:ignore # system - system_person = Person.objects.create(name="(System)", ascii="(System)", address="") + system_person = Person.objects.create(name="(System)", ascii="(System)") Email.objects.create(address="", person=system_person) # high-level groups From c7d31b44c3873aea57effb8bd5d81d16c50c1b32 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Fri, 27 Apr 2018 17:36:20 +0000 Subject: [PATCH 04/29] Added django-simple-history and replaced the old (and unused) PersonHistory class with a history=HistoricalRecords() field on Person. Added the needed migrations and changes to admin, resources, and settings. Related to issues #2505 and #2507. - Legacy-Id: 15096 --- ietf/help/tests_views.py | 6 +- ietf/person/admin.py | 23 ++++---- .../migrations/0004_auto_20180427_0646.py | 55 +++++++++++++++++++ ietf/person/models.py | 18 +++--- ietf/person/resources.py | 55 ++++++++++--------- ietf/settings.py | 2 + requirements.txt | 1 + 7 files changed, 112 insertions(+), 48 deletions(-) create mode 100644 ietf/person/migrations/0004_auto_20180427_0646.py diff --git a/ietf/help/tests_views.py b/ietf/help/tests_views.py index 2d6207ea2..714dab39e 100644 --- a/ietf/help/tests_views.py +++ b/ietf/help/tests_views.py @@ -7,7 +7,7 @@ import debug # pyflakes:ignore from ietf.utils.test_utils import TestCase from ietf.doc.models import StateType -class StateHelpTest(TestCase): +class HelpPageTests(TestCase): def test_state_index(self): url = reverse('ietf.help.views.state_index') @@ -21,3 +21,7 @@ class StateHelpTest(TestCase): self.assertIn(name, content) + def test_personal_information_help(self): + r = self.client.get('/help/personal-information') + self.assertContains(r, 'personal information') + self.assertContains(r, 'GDPR') diff --git a/ietf/person/admin.py b/ietf/person/admin.py index 86c0c851f..53978fae4 100644 --- a/ietf/person/admin.py +++ b/ietf/person/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from ietf.person.models import Email, Alias, Person, PersonHistory, PersonalApiKey, PersonEvent, PersonApiKeyEvent +from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent, HistoricalPerson from ietf.person.name import name_parts class EmailAdmin(admin.ModelAdmin): @@ -33,16 +33,6 @@ class PersonAdmin(admin.ModelAdmin): # actions = None admin.site.register(Person, PersonAdmin) -class PersonHistoryAdmin(admin.ModelAdmin): - def plain_name(self, obj): - prefix, first, middle, last, suffix = name_parts(obj.name) - return "%s %s" % (first, last) - list_display = ['name', 'short', 'plain_name', 'time', 'user', 'person', ] - list_filter = ['time'] - raw_id_fields = ['person', 'user'] - search_fields = ['name', 'ascii'] -admin.site.register(PersonHistory, PersonHistoryAdmin) - class PersonalApiKeyAdmin(admin.ModelAdmin): list_display = ['id', 'person', 'created', 'endpoint', 'valid', 'count', 'latest', ] list_filter = ['endpoint', 'created', ] @@ -61,3 +51,14 @@ class PersonApiKeyEventAdmin(admin.ModelAdmin): search_fields = ["person__name", ] raw_id_fields = ['person', ] admin.site.register(PersonApiKeyEvent, PersonApiKeyEventAdmin) + + +class HistoricalPersonAdmin(admin.ModelAdmin): + def plain_name(self, obj): + prefix, first, middle, last, suffix = name_parts(obj.name) + return "%s %s" % (first, last) + list_display = ["history_date", "name", "plain_name", "time", "history_user", "history_change_reason", ] + search_fields = ["name", "ascii"] + raw_id_fields = ["user", "history_user", ] +# actions = None +admin.site.register(HistoricalPerson, HistoricalPersonAdmin) diff --git a/ietf/person/migrations/0004_auto_20180427_0646.py b/ietf/person/migrations/0004_auto_20180427_0646.py new file mode 100644 index 000000000..c9d83578c --- /dev/null +++ b/ietf/person/migrations/0004_auto_20180427_0646.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-27 06:46 +from __future__ import unicode_literals + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('person', '0003_auto_20180426_0517'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalPerson', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('time', models.DateTimeField(default=datetime.datetime.now)), + ('name', models.CharField(db_index=True, help_text=b'Preferred form of name.', max_length=255, verbose_name=b'Full Name (Unicode)')), + ('ascii', models.CharField(help_text=b'Name as rendered in ASCII (Latin, unaccented) characters.', max_length=255, verbose_name=b'Full Name (ASCII)')), + ('ascii_short', models.CharField(blank=True, help_text=b'Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).', max_length=32, null=True, verbose_name=b'Abbreviated Name (ASCII)')), + ('affiliation', models.CharField(blank=True, help_text=b'Employer, university, sponsor, etc.', max_length=255)), + ('biography', models.TextField(blank=True, help_text=b'Short biography for use on leadership pages. Use plain text or reStructuredText markup.')), + ('photo', models.TextField(blank=True, default=None, max_length=100)), + ('photo_thumb', models.TextField(blank=True, default=None, max_length=100)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + 'verbose_name': 'historical person', + }, + ), + migrations.RemoveField( + model_name='personhistory', + name='person', + ), + migrations.RemoveField( + model_name='personhistory', + name='user', + ), + migrations.DeleteModel( + name='PersonHistory', + ), + ] diff --git a/ietf/person/models.py b/ietf/person/models.py index 36a51d00e..acfcbb528 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -16,6 +16,7 @@ from django.db import models from django.contrib.auth.models import User from django.template.loader import render_to_string from django.utils.text import slugify +from simple_history.models import HistoricalRecords import debug # pyflakes:ignore @@ -27,7 +28,9 @@ from ietf.person.name import unidecode_name from ietf.utils.models import ForeignKey, OneToOneField -class PersonInfo(models.Model): +class Person(models.Model): + history = HistoricalRecords() + user = OneToOneField(User, blank=True, null=True) time = models.DateTimeField(default=datetime.datetime.now) # When this Person record entered the system # The normal unicode form of the name. This must be # set to the same value as the ascii-form if equal. @@ -143,24 +146,21 @@ class PersonInfo(models.Model): def has_drafts(self): from ietf.doc.models import Document return Document.objects.filter(documentauthor__person=self, type='draft').exists() + def rfcs(self): from ietf.doc.models import Document rfcs = list(Document.objects.filter(documentauthor__person=self, type='draft', states__slug='rfc')) rfcs.sort(key=lambda d: d.canonical_name() ) return rfcs + def active_drafts(self): from ietf.doc.models import Document return Document.objects.filter(documentauthor__person=self, type='draft', states__slug='active').order_by('-time') + def expired_drafts(self): from ietf.doc.models import Document return Document.objects.filter(documentauthor__person=self, type='draft', states__slug__in=['repl', 'expired', 'auth-rm', 'ietf-rm']).order_by('-time') - class Meta: - abstract = True - -class Person(PersonInfo): - user = OneToOneField(User, blank=True, null=True) - def save(self, *args, **kwargs): created = not self.pk super(Person, self).save(*args, **kwargs) @@ -198,10 +198,6 @@ class Person(PersonInfo): ct1['affiliation']= self.affiliation return ct1 -class PersonHistory(PersonInfo): - person = ForeignKey(Person, related_name="history_set") - user = ForeignKey(User, blank=True, null=True) - class Alias(models.Model): """This is used for alternative forms of a name. This is the primary lookup point for names, and should always contain the diff --git a/ietf/person/resources.py b/ietf/person/resources.py index 1e842a69f..d2426a654 100644 --- a/ietf/person/resources.py +++ b/ietf/person/resources.py @@ -6,7 +6,7 @@ from tastypie.cache import SimpleCache from ietf import api -from ietf.person.models import (Person, Email, Alias, PersonHistory, PersonalApiKey, PersonEvent, PersonApiKeyEvent) +from ietf.person.models import (Person, Email, Alias, PersonalApiKey, PersonEvent, PersonApiKeyEvent, HistoricalPerson) from ietf.utils.resources import UserResource @@ -24,7 +24,6 @@ class PersonResource(ModelResource): "name": ALL, "ascii": ALL, "ascii_short": ALL, - "address": ALL, "affiliation": ALL, "photo": ALL, "biography": ALL, @@ -61,29 +60,6 @@ class AliasResource(ModelResource): } api.person.register(AliasResource()) -from ietf.utils.resources import UserResource -class PersonHistoryResource(ModelResource): - person = ToOneField(PersonResource, 'person') - user = ToOneField(UserResource, 'user', null=True) - class Meta: - cache = SimpleCache() - queryset = PersonHistory.objects.all() - serializer = api.Serializer() - #resource_name = 'personhistory' - filtering = { - "id": ALL, - "time": ALL, - "name": ALL, - "ascii": ALL, - "ascii_short": ALL, - "address": ALL, - "affiliation": ALL, - "person": ALL_WITH_RELATIONS, - "user": ALL_WITH_RELATIONS, - } -api.person.register(PersonHistoryResource()) - - class PersonalApiKeyResource(ModelResource): person = ToOneField(PersonResource, 'person') class Meta: @@ -141,3 +117,32 @@ class PersonApiKeyEventResource(ModelResource): "key": ALL_WITH_RELATIONS, } api.person.register(PersonApiKeyEventResource()) + + +from ietf.utils.resources import UserResource +class HistoricalPersonResource(ModelResource): + user = ToOneField(UserResource, 'user', null=True) + history_user = ToOneField(UserResource, 'history_user', null=True) + class Meta: + queryset = HistoricalPerson.objects.all() + serializer = api.Serializer() + cache = SimpleCache() + #resource_name = 'historicalperson' + filtering = { + "id": ALL, + "time": ALL, + "name": ALL, + "ascii": ALL, + "ascii_short": ALL, + "affiliation": ALL, + "biography": ALL, + "photo": ALL, + "photo_thumb": ALL, + "history_id": ALL, + "history_date": ALL, + "history_change_reason": ALL, + "history_type": ALL, + "user": ALL_WITH_RELATIONS, + "history_user": ALL_WITH_RELATIONS, + } +api.person.register(HistoricalPersonResource()) diff --git a/ietf/settings.py b/ietf/settings.py index 62b3f7bd0..1651ee521 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -349,6 +349,7 @@ MIDDLEWARE = ( 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.http.ConditionalGetMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware', # 'ietf.middleware.sql_log_middleware', 'ietf.middleware.SMTPExceptionMiddleware', 'ietf.middleware.Utf8ExceptionMiddleware', @@ -385,6 +386,7 @@ INSTALLED_APPS = ( 'django_password_strength', 'djangobwr', 'form_utils', + 'simple_history', 'tastypie', 'widget_tweaks', # IETF apps diff --git a/requirements.txt b/requirements.txt index 0b6ff25cb..98ef5c19c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ django-bootstrap3>=8.2.1,<9.0.0 django-formtools>=1.0 # instead of django.contrib.formtools in 1.8 django-markup>=1.1 django-password-strength>=1.2.1 +django-simple-history>=2.0 django-tastypie>=0.13.2 django-widget-tweaks>=1.3 docutils>=0.12 From 61a16856adf6ad4b9930942cbd1047418cbd38d8 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Tue, 1 May 2018 11:07:12 +0000 Subject: [PATCH 05/29] Updated the admin interface to use the simple-history admin support for Person history. - Legacy-Id: 15097 --- ietf/person/admin.py | 15 +++------------ ietf/utils/tests.py | 7 +++++-- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/ietf/person/admin.py b/ietf/person/admin.py index 53978fae4..9f8d68482 100644 --- a/ietf/person/admin.py +++ b/ietf/person/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin +import simple_history - -from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent, HistoricalPerson +from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent from ietf.person.name import name_parts class EmailAdmin(admin.ModelAdmin): @@ -22,7 +22,7 @@ admin.site.register(Alias, AliasAdmin) class AliasInline(admin.StackedInline): model = Alias -class PersonAdmin(admin.ModelAdmin): +class PersonAdmin(simple_history.admin.SimpleHistoryAdmin): def plain_name(self, obj): prefix, first, middle, last, suffix = name_parts(obj.name) return "%s %s" % (first, last) @@ -53,12 +53,3 @@ class PersonApiKeyEventAdmin(admin.ModelAdmin): admin.site.register(PersonApiKeyEvent, PersonApiKeyEventAdmin) -class HistoricalPersonAdmin(admin.ModelAdmin): - def plain_name(self, obj): - prefix, first, middle, last, suffix = name_parts(obj.name) - return "%s %s" % (first, last) - list_display = ["history_date", "name", "plain_name", "time", "history_user", "history_change_reason", ] - search_fields = ["name", "ascii"] - raw_id_fields = ["user", "history_user", ] -# actions = None -admin.site.register(HistoricalPerson, HistoricalPersonAdmin) diff --git a/ietf/utils/tests.py b/ietf/utils/tests.py index e440c652e..778f0f0bc 100644 --- a/ietf/utils/tests.py +++ b/ietf/utils/tests.py @@ -307,8 +307,11 @@ class AdminTestCase(TestCase): # model_list = apps.get_app_config(name).get_models() for model in model_list: - self.assertContains(r, model._meta.model_name, - msg_prefix="There doesn't seem to be any admin API for model %s.models.%s"%(app.__name__,model.__name__,)) + if model.__name__.startswith('Historical') and hasattr(model, "get_history_type_display"): + continue + else: + self.assertContains(r, model._meta.model_name, + msg_prefix="There doesn't seem to be any admin API for model %s.models.%s"%(app.__name__,model.__name__,)) ## One might think that the code below would work, but it doesn't ... From 37f0d141e9bd36a97ee118ff670960903bafdc0e Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 5 May 2018 12:37:15 +0000 Subject: [PATCH 06/29] Added a new field name_from_draft to Person, to hold the name field equivalent as captured from drafts, in case name has been modified by the user and we're asked to remove that info under GDPR. Added history for Email, and also an origin field to capture from where we got an email address (draft name, username, meeting registration, etc.) Added a log.assertion() to Email.save() in order to ensure we don't create any email without setting origin. - Legacy-Id: 15126 --- .../migrations/0003_auto_20180426_0517.py | 23 --------- ...427_0646.py => 0003_auto_20180504_1519.py} | 51 +++++++++++++++++-- ietf/person/models.py | 13 +++-- 3 files changed, 58 insertions(+), 29 deletions(-) delete mode 100644 ietf/person/migrations/0003_auto_20180426_0517.py rename ietf/person/migrations/{0004_auto_20180427_0646.py => 0003_auto_20180504_1519.py} (52%) diff --git a/ietf/person/migrations/0003_auto_20180426_0517.py b/ietf/person/migrations/0003_auto_20180426_0517.py deleted file mode 100644 index 0cad90d03..000000000 --- a/ietf/person/migrations/0003_auto_20180426_0517.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.12 on 2018-04-26 05:17 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('person', '0002_auto_20180330_0808'), - ] - - operations = [ - migrations.RemoveField( - model_name='person', - name='address', - ), - migrations.RemoveField( - model_name='personhistory', - name='address', - ), - ] diff --git a/ietf/person/migrations/0004_auto_20180427_0646.py b/ietf/person/migrations/0003_auto_20180504_1519.py similarity index 52% rename from ietf/person/migrations/0004_auto_20180427_0646.py rename to ietf/person/migrations/0003_auto_20180504_1519.py index c9d83578c..1bbedbbec 100644 --- a/ietf/person/migrations/0004_auto_20180427_0646.py +++ b/ietf/person/migrations/0003_auto_20180504_1519.py @@ -1,21 +1,43 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.12 on 2018-04-27 06:46 +# Generated by Django 1.11.12 on 2018-05-04 15:19 from __future__ import unicode_literals import datetime from django.conf import settings +import django.core.validators from django.db import migrations, models import django.db.models.deletion +import ietf.utils.models class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('person', '0003_auto_20180426_0517'), + ('person', '0002_auto_20180330_0808'), ] operations = [ + migrations.CreateModel( + name='HistoricalEmail', + fields=[ + ('address', models.CharField(db_index=True, max_length=64, validators=[django.core.validators.EmailValidator()])), + ('time', models.DateTimeField(blank=True, editable=False)), + ('primary', models.BooleanField(default=False)), + ('origin', models.CharField(default=b'', editable=False, max_length=150)), + ('active', models.BooleanField(default=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + 'verbose_name': 'historical email', + }, + ), migrations.CreateModel( name='HistoricalPerson', fields=[ @@ -24,10 +46,10 @@ class Migration(migrations.Migration): ('name', models.CharField(db_index=True, help_text=b'Preferred form of name.', max_length=255, verbose_name=b'Full Name (Unicode)')), ('ascii', models.CharField(help_text=b'Name as rendered in ASCII (Latin, unaccented) characters.', max_length=255, verbose_name=b'Full Name (ASCII)')), ('ascii_short', models.CharField(blank=True, help_text=b'Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).', max_length=32, null=True, verbose_name=b'Abbreviated Name (ASCII)')), - ('affiliation', models.CharField(blank=True, help_text=b'Employer, university, sponsor, etc.', max_length=255)), ('biography', models.TextField(blank=True, help_text=b'Short biography for use on leadership pages. Use plain text or reStructuredText markup.')), ('photo', models.TextField(blank=True, default=None, max_length=100)), ('photo_thumb', models.TextField(blank=True, default=None, max_length=100)), + ('name_from_draft', models.CharField(editable=False, help_text=b'Name as found in a draft submission.', max_length=255, null=True, verbose_name=b'Full Name (from submission)')), ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField()), ('history_change_reason', models.CharField(max_length=100, null=True)), @@ -49,7 +71,30 @@ class Migration(migrations.Migration): model_name='personhistory', name='user', ), + migrations.RemoveField( + model_name='person', + name='address', + ), + migrations.RemoveField( + model_name='person', + name='affiliation', + ), + migrations.AddField( + model_name='email', + name='origin', + field=models.CharField(default=b'', editable=False, max_length=150), + ), + migrations.AddField( + model_name='person', + name='name_from_draft', + field=models.CharField(editable=False, help_text=b'Name as found in a draft submission.', max_length=255, null=True, verbose_name=b'Full Name (from submission)'), + ), migrations.DeleteModel( name='PersonHistory', ), + migrations.AddField( + model_name='historicalemail', + name='person', + field=ietf.utils.models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='person.Person'), + ), ] diff --git a/ietf/person/models.py b/ietf/person/models.py index acfcbb528..674d6fc5f 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -25,6 +25,7 @@ from ietf.utils.mail import send_mail_preformatted from ietf.utils.storage import NoLocationMigrationFileSystemStorage from ietf.utils.mail import formataddr from ietf.person.name import unidecode_name +from ietf.utils import log from ietf.utils.models import ForeignKey, OneToOneField @@ -39,10 +40,10 @@ class Person(models.Model): ascii = models.CharField("Full Name (ASCII)", max_length=255, help_text="Name as rendered in ASCII (Latin, unaccented) characters.") # The short ascii-form of the name. Also in alias table if non-null ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True, help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).") - affiliation = models.CharField(max_length=255, blank=True, help_text="Employer, university, sponsor, etc.") biography = models.TextField(blank=True, help_text="Short biography for use on leadership pages. Use plain text or reStructuredText markup.") photo = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) photo_thumb = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) + name_from_draft = models.CharField("Full Name (from submission)", null=True, max_length=255, editable=False, help_text="Name as found in a draft submission.") def __unicode__(self): return self.plain_name() @@ -195,7 +196,6 @@ class Person(models.Model): ct1['href'] = urljoin(hostscheme, self.json_url()) ct1['name'] = self.name ct1['ascii'] = self.ascii - ct1['affiliation']= self.affiliation return ct1 class Alias(models.Model): @@ -226,12 +226,15 @@ class Alias(models.Model): verbose_name_plural = "Aliases" class Email(models.Model): + history = HistoricalRecords() address = models.CharField(max_length=64, primary_key=True, validators=[validate_email]) person = ForeignKey(Person, null=True) time = models.DateTimeField(auto_now_add=True) primary = models.BooleanField(default=False) + origin = models.CharField(max_length=150, default='', editable=False) # User.username or Document.name active = models.BooleanField(default=True) # Old email addresses are *not* purged, as history - # information points to persons through these + # information points to persons through these + def __unicode__(self): return self.address or "Email object with id: %s"%self.pk @@ -275,6 +278,10 @@ class Email(models.Model): return return self.address + def save(self, *args, **kwargs): + if not self.origin: + log.assertion('self.origin') + super(Email, self).save(*args, **kwargs) # "{key.id}{salt}{hash} KEY_STRUCT = "i12s32s" From 5f37a7188926a7275063ccd2fea3dcf7b6449438 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 5 May 2018 12:40:30 +0000 Subject: [PATCH 07/29] Added origin information to all places where we create email address entries. - Legacy-Id: 15127 --- ietf/doc/tests_draft.py | 10 +++++----- ietf/ietfauth/tests.py | 8 ++++---- ietf/ietfauth/views.py | 4 ++-- .../management/commands/make_dummy_nomcom.py | 6 +++--- ietf/nomcom/test_data.py | 2 +- ietf/nomcom/tests.py | 8 ++++---- ietf/nomcom/utils.py | 2 +- ietf/person/factories.py | 4 ++-- ietf/person/tests.py | 6 +++--- ietf/review/import_from_review_tool.py | 2 +- ietf/stats/utils.py | 10 ++++------ ietf/submit/utils.py | 10 +++++----- ietf/utils/test_data.py | 14 +++++++------- 13 files changed, 42 insertions(+), 44 deletions(-) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 3c94e4723..cb2e36200 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -1001,7 +1001,7 @@ class IndividualInfoFormsTests(TestCase): doc.shepherd = Email.objects.get(person__user__username="plain") doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_shepherd", by=Person.objects.get(user__username="secretary"), desc="Test")]) - new_email = Email.objects.create(address="anotheremail@example.com", person=doc.shepherd.person) + new_email = Email.objects.create(address="anotheremail@example.com", person=doc.shepherd.person, origin='test') r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -1436,7 +1436,7 @@ class ChangeReplacesTests(TestCase): group=mars_wg, ) p = Person.objects.create(name="basea_author") - e = Email.objects.create(address="basea_author@example.com", person=p) + e = Email.objects.create(address="basea_author@example.com", person=p, origin='test') self.basea.documentauthor_set.create(person=p, email=e, order=1) self.baseb = Document.objects.create( @@ -1449,7 +1449,7 @@ class ChangeReplacesTests(TestCase): group=mars_wg, ) p = Person.objects.create(name="baseb_author") - e = Email.objects.create(address="baseb_author@example.com", person=p) + e = Email.objects.create(address="baseb_author@example.com", person=p, origin='test') self.baseb.documentauthor_set.create(person=p, email=e, order=1) self.replacea = Document.objects.create( @@ -1462,7 +1462,7 @@ class ChangeReplacesTests(TestCase): group=mars_wg, ) p = Person.objects.create(name="replacea_author") - e = Email.objects.create(address="replacea_author@example.com", person=p) + e = Email.objects.create(address="replacea_author@example.com", person=p, origin='test') self.replacea.documentauthor_set.create(person=p, email=e, order=1) self.replaceboth = Document.objects.create( @@ -1475,7 +1475,7 @@ class ChangeReplacesTests(TestCase): group=mars_wg, ) p = Person.objects.create(name="replaceboth_author") - e = Email.objects.create(address="replaceboth_author@example.com", person=p) + e = Email.objects.create(address="replaceboth_author@example.com", person=p, origin='test') self.replaceboth.documentauthor_set.create(person=p, email=e, order=1) self.basea.set_state(State.objects.get(used=True, type="draft", slug="active")) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 270202d8f..7d255df99 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -308,7 +308,7 @@ class IetfAuthTests(TestCase): user.set_password("forgotten") user.save() p = Person.objects.create(name="Some One", ascii="Some One", user=user) - Email.objects.create(address=user.username, person=p) + Email.objects.create(address=user.username, person=p, origin='test') # get r = self.client.get(url) @@ -418,7 +418,7 @@ class IetfAuthTests(TestCase): user.set_password("password") user.save() p = Person.objects.create(name="Some One", ascii="Some One", user=user) - Email.objects.create(address=user.username, person=p) + Email.objects.create(address=user.username, person=p, origin='test') # log in r = self.client.post(redir_url, {"username":user.username, "password":"password"}) @@ -465,8 +465,8 @@ class IetfAuthTests(TestCase): user.set_password("password") user.save() p = Person.objects.create(name="Some One", ascii="Some One", user=user) - Email.objects.create(address=user.username, person=p) - Email.objects.create(address="othername@example.org", person=p) + Email.objects.create(address=user.username, person=p, origin='test') + Email.objects.create(address="othername@example.org", person=p, origin='test') # log in r = self.client.post(redir_url, {"username":user.username, "password":"password"}) diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index e3f549279..56013119a 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -172,7 +172,7 @@ def confirm_account(request, auth): Alias.objects.create(person=person, name=name) if not email_obj: - email_obj = Email.objects.create(address=email, person=person) + email_obj = Email.objects.create(address=email, person=person, origin=user.username) else: if not email_obj.person: email_obj.person = person @@ -293,7 +293,7 @@ def confirm_new_email(request, auth): can_confirm = form.is_valid() and email new_email_obj = None if request.method == 'POST' and can_confirm and request.POST.get("action") == "confirm": - new_email_obj = Email.objects.create(address=email, person=person) + new_email_obj = Email.objects.create(address=email, person=person, origin=username) return render(request, 'registration/confirm_new_email.html', { 'username': username, diff --git a/ietf/nomcom/management/commands/make_dummy_nomcom.py b/ietf/nomcom/management/commands/make_dummy_nomcom.py index b91b91ee5..6adb62ddf 100644 --- a/ietf/nomcom/management/commands/make_dummy_nomcom.py +++ b/ietf/nomcom/management/commands/make_dummy_nomcom.py @@ -39,18 +39,18 @@ class Command(BaseCommand): populate_personnel=False, populate_positions=False)) - e = EmailFactory(person__name=u'Dummy Chair',address=u'dummychair@example.com',person__user__username=u'dummychair',person__default_emails=False) + e = EmailFactory(person__name=u'Dummy Chair', address=u'dummychair@example.com', person__user__username=u'dummychair', person__default_emails=False, origin='test') e.person.user.set_password('password') e.person.user.save() nc.group.role_set.create(name_id=u'chair',person=e.person,email=e) - e = EmailFactory(person__name=u'Dummy Member',address=u'dummymember@example.com',person__user__username=u'dummymember',person__default_emails=False) + e = EmailFactory(person__name=u'Dummy Member', address=u'dummymember@example.com', person__user__username=u'dummymember', person__default_emails=False, origin='test') e.person.user.set_password('password') e.person.user.save() nc.group.role_set.create(name_id=u'member',person=e.person,email=e) - e = EmailFactory(person__name=u'Dummy Candidate',address=u'dummycandidate@example.com',person__user__username=u'dummycandidate',person__default_emails=False) + e = EmailFactory(person__name=u'Dummy Candidate', address=u'dummycandidate@example.com', person__user__username=u'dummycandidate', person__default_emails=False, origin='test') e.person.user.set_password('password') e.person.user.save() NomineePositionFactory(nominee__nomcom=nc, nominee__person=e.person, diff --git a/ietf/nomcom/test_data.py b/ietf/nomcom/test_data.py index 759fe1e39..70dad80f2 100644 --- a/ietf/nomcom/test_data.py +++ b/ietf/nomcom/test_data.py @@ -123,7 +123,7 @@ def nomcom_test_data(): u.set_password(COMMUNITY_USER+"+password") u.save() plainman, _ = Person.objects.get_or_create(name="Plain Man", ascii="Plain Man", user=u) - email, _ = Email.objects.get_or_create(address="plain@example.com", person=plainman) + email, _ = Email.objects.get_or_create(address="plain@example.com", person=plainman, origin='test') nominee, _ = Nominee.objects.get_or_create(email=email, nomcom=nomcom) # positions diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 1720a8209..ca406663f 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -576,7 +576,7 @@ class NomcomViewsTest(TestCase): if not searched_email: searched_email = Email.objects.filter(address=nominee_email).first() if not searched_email: - searched_email = EmailFactory(address=nominee_email,primary=True) + searched_email = EmailFactory(address=nominee_email, primary=True, origin='test') if not searched_email.person: searched_email.person = PersonFactory() searched_email.save() @@ -967,8 +967,8 @@ class ReminderTest(TestCase): today = datetime.date.today() t_minus_3 = today - datetime.timedelta(days=3) t_minus_4 = today - datetime.timedelta(days=4) - e1 = EmailFactory(address="nominee1@example.org",person=PersonFactory(name=u"Nominee 1")) - e2 = EmailFactory(address="nominee2@example.org",person=PersonFactory(name=u"Nominee 2")) + e1 = EmailFactory(address="nominee1@example.org", person=PersonFactory(name=u"Nominee 1"), origin='test') + e2 = EmailFactory(address="nominee2@example.org", person=PersonFactory(name=u"Nominee 2"), origin='test') n = make_nomineeposition(self.nomcom,e1.person,gen,None) np = n.nomineeposition_set.get(position=gen) np.time = t_minus_3 @@ -1716,7 +1716,7 @@ Junk body for testing def test_edit_nominee(self): nominee = self.nc.nominee_set.order_by('pk').first() - new_email = EmailFactory(person=nominee.person) + new_email = EmailFactory(person=nominee.person, origin='test') url = reverse('ietf.nomcom.views.edit_nominee',kwargs={'year':self.nc.year(),'nominee_id':nominee.id}) login_testing_unauthorized(self,self.chair.user.username,url) response = self.client.get(url) diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 4d999da9b..d09436699 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -372,7 +372,7 @@ def make_nomineeposition(nomcom, candidate, position, author): def make_nomineeposition_for_newperson(nomcom, candidate_name, candidate_email, position, author): # This is expected to fail if called with an existing email address - email = Email.objects.create(address=candidate_email) + email = Email.objects.create(address=candidate_email, origin=nomcom.group.acronym) person = Person.objects.create(name=candidate_name, ascii=unidecode_name(candidate_name), ) diff --git a/ietf/person/factories.py b/ietf/person/factories.py index a9c02c5fb..95793c100 100644 --- a/ietf/person/factories.py +++ b/ietf/person/factories.py @@ -71,7 +71,7 @@ class PersonFactory(factory.DjangoModelFactory): extracted = True if create and extracted: make_email = getattr(EmailFactory, 'create' if create else 'build') - make_email(person=obj,address=obj.user.email) + make_email(person=obj, address=obj.user.email, origin='test') @factory.post_generation def default_photo(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument @@ -131,4 +131,4 @@ class EmailFactory(factory.DjangoModelFactory): active = True primary = False - + origin = '' diff --git a/ietf/person/tests.py b/ietf/person/tests.py index caf32beb4..690712f7a 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -42,9 +42,9 @@ class PersonTests(TestCase): def test_default_email(self): person = PersonFactory() - primary = EmailFactory(person=person,primary=True,active=True) - EmailFactory(person=person,primary=False,active=True) - EmailFactory(person=person,primary=False,active=False) + primary = EmailFactory(person=person, primary=True, active=True, origin='test') + EmailFactory(person=person, primary=False, active=True, origin='test') + EmailFactory(person=person, primary=False, active=False, origin='test') self.assertTrue(primary.address in person.formatted_email()) def test_profile(self): diff --git a/ietf/review/import_from_review_tool.py b/ietf/review/import_from_review_tool.py index 93635d813..a3059d3b0 100755 --- a/ietf/review/import_from_review_tool.py +++ b/ietf/review/import_from_review_tool.py @@ -101,7 +101,7 @@ with db_con.cursor() as c: for name in new_aliases: Alias.objects.create(person=person, name=name) - email, created = Email.objects.get_or_create(address=row.email, person=person) + email, created = Email.objects.get_or_create(address=row.email, person=person, origin=__name__) if created: print "created email", email diff --git a/ietf/stats/utils.py b/ietf/stats/utils.py index 85a11135e..c9a473aee 100644 --- a/ietf/stats/utils.py +++ b/ietf/stats/utils.py @@ -299,18 +299,16 @@ def get_meeting_registration_data(meeting): person = Person.objects.create( name=regname, ascii=ascii_name, - affiliation=affiliation, user=user, ) # Create an associated Email address for this Person - email, __ = Email.objects.get_or_create( - person=person, - address=address[:64], - ) + try: + email = Email.objects.get(person=person, address=address[:64]) + except Email.DoesNotExist: + email = Email.objects.create(person=person, address=address[:64], origin='ietf %s registration'%meeting.number) if email.address != address: debug.say("Truncated address: %s --> %s" % (address, email.address)) - # If this is the only email address, set primary to true. # If the person already existed (found through Alias) and diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index c40c8aa2d..7467021a4 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -136,7 +136,7 @@ def docevent_from_submission(request, submission, desc, who=None): else: submitter_parsed = submission.submitter_parsed() if submitter_parsed["name"] and submitter_parsed["email"]: - by, _ = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]) + by, _ = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"], submission.name) else: by = system @@ -190,7 +190,7 @@ def post_submission(request, submission, approvedDesc): system = Person.objects.get(name="(System)") submitter_parsed = submission.submitter_parsed() if submitter_parsed["name"] and submitter_parsed["email"]: - submitter, _ = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]) + submitter, _ = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"], submission.name) submitter_info = u'%s <%s>' % (submitter_parsed["name"], submitter_parsed["email"]) else: submitter = system @@ -428,7 +428,7 @@ def get_person_from_name_email(name, email): return None -def ensure_person_email_info_exists(name, email): +def ensure_person_email_info_exists(name, email, docname): addr = email email = None person = get_person_from_name_email(name, addr) @@ -461,7 +461,7 @@ def ensure_person_email_info_exists(name, email): email = Email.objects.get(address=addr,person__isnull=True) except Email.DoesNotExist: # most likely we just need to create it - email = Email(address=addr) + email = Email(address=addr, origin=docname) email.active = active email.person = person @@ -474,7 +474,7 @@ def ensure_person_email_info_exists(name, email): def update_authors(draft, submission): persons = [] for order, author in enumerate(submission.authors): - person, email = ensure_person_email_info_exists(author["name"], author.get("email")) + person, email = ensure_person_email_info_exists(author["name"], author.get("email"), submission.name) a = DocumentAuthor.objects.filter(document=draft, person=person).first() if not a: diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index a8a56a8d4..de3a48be5 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -38,7 +38,7 @@ def create_person(group, role_name, name=None, username=None, email_address=None user.set_password(password) user.save() person = Person.objects.create(name=name, ascii=unidecode_name(smart_text(name)), user=user) - email = Email.objects.create(address=email_address, person=person) + email = Email.objects.create(address=email_address, person=person, origin='test') Role.objects.create(group=group, name_id=role_name, person=person, email=email) return person @@ -61,7 +61,7 @@ def make_immutable_base_data(): # system system_person = Person.objects.create(name="(System)", ascii="(System)") - Email.objects.create(address="", person=system_person) + Email.objects.create(address="", person=system_person, origin='test') # high-level groups ietf = create_group(name="IETF", acronym="ietf", type_id="ietf") @@ -112,7 +112,7 @@ def make_immutable_base_data(): for i in range(1, 10): u = User.objects.create(username="ad%s" % i) p = Person.objects.create(name="Ad No%s" % i, ascii="Ad No%s" % i, user=u) - email = Email.objects.create(address="ad%s@ietf.org" % i, person=p) + email = Email.objects.create(address="ad%s@ietf.org" % i, person=p, origin='test') if i < 6: # active Role.objects.create(name_id="ad", group=area, person=p, email=email) @@ -232,7 +232,7 @@ def make_test_data(): u.set_password("plain+password") u.save() plainman = Person.objects.create(name="Plain Man", ascii="Plain Man", user=u) - email = Email.objects.create(address="plain@example.com", person=plainman) + email = Email.objects.create(address="plain@example.com", person=plainman, origin='test') # group personnel create_person(mars_wg, "chair", name="WG Cháir Man", username="marschairman") @@ -473,7 +473,7 @@ def make_review_data(doc): u.set_password("reviewer+password") u.save() reviewer = Person.objects.create(name=u"Some Réviewer", ascii="Some Reviewer", user=u) - email = Email.objects.create(address="reviewer@example.com", person=reviewer) + email = Email.objects.create(address="reviewer@example.com", person=reviewer, origin='test') for team in (team1, team2, team3): Role.objects.create(name_id="reviewer", person=reviewer, email=email, group=team) @@ -496,14 +496,14 @@ def make_review_data(doc): u.set_password("reviewsecretary+password") u.save() reviewsecretary = Person.objects.create(name=u"Réview Secretary", ascii="Review Secretary", user=u) - reviewsecretary_email = Email.objects.create(address="reviewsecretary@example.com", person=reviewsecretary) + reviewsecretary_email = Email.objects.create(address="reviewsecretary@example.com", person=reviewsecretary, origin='test') Role.objects.create(name_id="secr", person=reviewsecretary, email=reviewsecretary_email, group=team1) u = User.objects.create(username="reviewsecretary3") u.set_password("reviewsecretary3+password") u.save() reviewsecretary3 = Person.objects.create(name=u"Réview Secretary3", ascii="Review Secretary3", user=u) - reviewsecretary3_email = Email.objects.create(address="reviewsecretary3@example.com", person=reviewsecretary) + reviewsecretary3_email = Email.objects.create(address="reviewsecretary3@example.com", person=reviewsecretary, origin='test') Role.objects.create(name_id="secr", person=reviewsecretary3, email=reviewsecretary3_email, group=team3) return review_req From 9fe73b57360a1883f47f0300d08642b293a72db5 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 5 May 2018 12:41:47 +0000 Subject: [PATCH 08/29] Updated admin and resources with email history entries. - Legacy-Id: 15128 --- ietf/person/admin.py | 2 +- ietf/person/resources.py | 27 ++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ietf/person/admin.py b/ietf/person/admin.py index 9f8d68482..ed50eaeaa 100644 --- a/ietf/person/admin.py +++ b/ietf/person/admin.py @@ -4,7 +4,7 @@ import simple_history from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent from ietf.person.name import name_parts -class EmailAdmin(admin.ModelAdmin): +class EmailAdmin(simple_history.admin.SimpleHistoryAdmin): list_display = ["address", "person", "time", "active", ] raw_id_fields = ["person", ] search_fields = ["address", "person__name", ] diff --git a/ietf/person/resources.py b/ietf/person/resources.py index d2426a654..c5b51e6c4 100644 --- a/ietf/person/resources.py +++ b/ietf/person/resources.py @@ -6,7 +6,7 @@ from tastypie.cache import SimpleCache from ietf import api -from ietf.person.models import (Person, Email, Alias, PersonalApiKey, PersonEvent, PersonApiKeyEvent, HistoricalPerson) +from ietf.person.models import (Person, Email, Alias, PersonalApiKey, PersonEvent, PersonApiKeyEvent, HistoricalPerson, HistoricalEmail) from ietf.utils.resources import UserResource @@ -146,3 +146,28 @@ class HistoricalPersonResource(ModelResource): "history_user": ALL_WITH_RELATIONS, } api.person.register(HistoricalPersonResource()) + + +from ietf.utils.resources import UserResource +class HistoricalEmailResource(ModelResource): + person = ToOneField(PersonResource, 'person', null=True) + history_user = ToOneField(UserResource, 'history_user', null=True) + class Meta: + queryset = HistoricalEmail.objects.all() + serializer = api.Serializer() + cache = SimpleCache() + #resource_name = 'historicalemail' + filtering = { + "address": ALL, + "time": ALL, + "primary": ALL, + "origin": ALL, + "active": ALL, + "history_id": ALL, + "history_date": ALL, + "history_change_reason": ALL, + "history_type": ALL, + "person": ALL_WITH_RELATIONS, + "history_user": ALL_WITH_RELATIONS, + } +api.person.register(HistoricalEmailResource()) From a66639299da119f2f79bcd905576028674304454 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 5 May 2018 12:47:55 +0000 Subject: [PATCH 09/29] Removed all references to the removed Person.affiliation field. - Legacy-Id: 15129 --- ietf/doc/views_search.py | 1 - ietf/person/utils.py | 2 +- ietf/secr/rolodex/tests.py | 1 - ietf/secr/templates/includes/search_results_table.html | 2 -- ietf/secr/templates/rolodex/view.html | 2 -- ietf/templates/person/person_info.html | 8 +------- ietf/templates/utils/merge_person_records.txt | 2 -- 7 files changed, 2 insertions(+), 16 deletions(-) diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index ccfbd136b..51f9f27d0 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -175,7 +175,6 @@ def retrieve_search_results(form, all_types=False): if by == "author": docs = docs.filter( Q(documentauthor__person__alias__name__icontains=query["author"]) | - Q(documentauthor__person__affiliation__icontains=query["author"]) | Q(documentauthor__person__email__address__icontains=query["author"]) ) elif by == "group": diff --git a/ietf/person/utils.py b/ietf/person/utils.py index ab8382322..51bae8a21 100755 --- a/ietf/person/utils.py +++ b/ietf/person/utils.py @@ -34,7 +34,7 @@ def merge_persons(source, target, file=sys.stdout, verbose=False): dedupe_aliases(target) # copy other attributes - for field in ('ascii','ascii_short','affiliation'): + for field in ('ascii','ascii_short', 'biography', 'photo', 'photo_thumb', 'name_from_draft'): if getattr(source,field) and not getattr(target,field): setattr(target,field,getattr(source,field)) target.save() diff --git a/ietf/secr/rolodex/tests.py b/ietf/secr/rolodex/tests.py index 9616e8f66..a4df74453 100644 --- a/ietf/secr/rolodex/tests.py +++ b/ietf/secr/rolodex/tests.py @@ -61,7 +61,6 @@ class RolodexTestCase(TestCase): 'name': person.name, 'ascii': person.ascii, 'ascii_short': person.ascii_short, - 'affiliation': person.affiliation, 'user': user.username, 'email-0-person':person.pk, 'email-0-address': person.email_address(), diff --git a/ietf/secr/templates/includes/search_results_table.html b/ietf/secr/templates/includes/search_results_table.html index dc23455fc..97abd16f3 100644 --- a/ietf/secr/templates/includes/search_results_table.html +++ b/ietf/secr/templates/includes/search_results_table.html @@ -2,7 +2,6 @@ <thead> <tr> <th>Name</th> - <th>Company</th> <th>Email</th> <th>ID</th> </tr> @@ -11,7 +10,6 @@ {% for item in results %} <tr class="{% cycle 'row1' 'row2' %}"> <td><a href="{% url 'ietf.secr.rolodex.views.view' id=item.person.id %}">{{item.name}}</a></td> - <td>{{item.person.affiliation}}</td> <td>{{item.person.email_address}}</td> <td>{{item.person.id}}</a></td> </tr> diff --git a/ietf/secr/templates/rolodex/view.html b/ietf/secr/templates/rolodex/view.html index 32eb4d506..6bfa0b45f 100644 --- a/ietf/secr/templates/rolodex/view.html +++ b/ietf/secr/templates/rolodex/view.html @@ -18,8 +18,6 @@ <tr><td>Ascii Name:</td><td>{{ person.ascii }}</td></tr> <tr><td>Short Name:</td><td>{{ person.ascii_short }}</td></tr> <tr><td>Aliases:</td><td>{% for alias in person.alias_set.all %}{% if not forloop.first %}, {% endif %}{{ alias.name }}{% endfor %} - <tr><td>Address:</td><td>{{ person.address }}</td></tr> - <tr><td>Affiliation:</td><td>{{ person.affiliation }}</td></tr> <tr><td>User:</td><td>{{ person.user }}</td></tr> <tr></tr> {% for email in person.emails %} diff --git a/ietf/templates/person/person_info.html b/ietf/templates/person/person_info.html index c1a800bc4..fc47969e7 100644 --- a/ietf/templates/person/person_info.html +++ b/ietf/templates/person/person_info.html @@ -2,12 +2,6 @@ <div class="row"> <div class="col-md-2">Name:</div><div class="col-md-10">{{ person.name }}</div> </div> - <div class="row"> - <div class="col-md-2">Address:</div><div class="col-md-10">{{ person.address }}</div> - </div> - <div class="row"> - <div class="col-md-2">Affiliation:</div><div class="col-md-10">{{ person.affiliation}}</div> - </div> <div class="row"> <div class="col-md-2">Login:</div><div class="col-md-10">{% if person.user %}{{ person.user }} (last used: {% if person.user.last_login %}{{ person.user.last_login|date:"Y-m-d" }}{% else %}never{% endif %}){% endif %}</div> </div> @@ -19,4 +13,4 @@ <div class="row"> <div class="col-md-2">Role{{ person.role_set.count|pluralize }}:</div><div class="col-md-10">{% for role in person.role_set.all %}{{ role.name }} {{ role.group.acronym }}{% if not forloop.last %}, {% endif %}{% endfor %}</div> </div> - </div> \ No newline at end of file + </div> diff --git a/ietf/templates/utils/merge_person_records.txt b/ietf/templates/utils/merge_person_records.txt index 2673a1181..69c8e7bdd 100644 --- a/ietf/templates/utils/merge_person_records.txt +++ b/ietf/templates/utils/merge_person_records.txt @@ -7,8 +7,6 @@ The merged record is: Name: {{ person.plain_name }} Aliases: {{ person.alias_set.all|join:", " }} -Address: {{ person.address }} -Affiliation: {{ person.affiliation }} User (login): {{ person.user.username }} Emails: From f0c0753e28c08f60f5c3d27076fdc7ba90b30a3b Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 5 May 2018 12:49:10 +0000 Subject: [PATCH 10/29] Added email origin information to some function calls that needed it. - Legacy-Id: 15130 --- ietf/secr/rolodex/views.py | 4 +++- ietf/submit/tests.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ietf/secr/rolodex/views.py b/ietf/secr/rolodex/views.py index 03f8fc48a..be3cd5b02 100644 --- a/ietf/secr/rolodex/views.py +++ b/ietf/secr/rolodex/views.py @@ -86,7 +86,9 @@ def add_proceed(request): # save email Email.objects.create(address=email, - person=person) + person=person, + origin=request.user.username, + ) # in theory a user record could exist which wasn't associated with a Person try: diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 57420620b..d4e3746c7 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -324,7 +324,7 @@ class SubmitTests(TestCase): prev_author = draft.documentauthor_set.all()[0] if change_authors: # Make it such that one of the previous authors has an invalid email address - bogus_person, bogus_email = ensure_person_email_info_exists(u'Bogus Person',None) + bogus_person, bogus_email = ensure_person_email_info_exists(u'Bogus Person', None, draft.name) DocumentAuthor.objects.create(document=draft, person=bogus_person, email=bogus_email, order=draft.documentauthor_set.latest('order').order+1) # pretend IANA reviewed it From df7df69a59008894b8b78e1ef4ff43b4c03c3632 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 5 May 2018 12:49:39 +0000 Subject: [PATCH 11/29] Updated comment text - Legacy-Id: 15131 --- ietf/utils/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/utils/history.py b/ietf/utils/history.py index 3cb33f5db..85e65024d 100644 --- a/ietf/utils/history.py +++ b/ietf/utils/history.py @@ -1,6 +1,6 @@ def find_history_active_at(obj, time): """Assumes obj has a corresponding history model (e.g. obj could - be Person with a corresponding PersonHistory model), then either + be Document with a corresponding DocHistory model), then either returns the object itself if it was active at time, or the history object active at time, or None if time predates the object and its history (assuming history is complete). From dda9c0136c99b106b6103d780e63ccb1dff3d74d Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Tue, 8 May 2018 16:23:27 +0000 Subject: [PATCH 12/29] Overwrite earlier email origin when we've picked up the address from a submission. - Legacy-Id: 15141 --- ietf/submit/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 7467021a4..836fb6d36 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -452,6 +452,8 @@ def ensure_person_email_info_exists(name, email, docname): try: email = person.email_set.get(address=addr) + email.origin = docname # overwrite earlier origin + email.save() except Email.DoesNotExist: try: # An Email object pointing to some other person will not exist @@ -461,12 +463,12 @@ def ensure_person_email_info_exists(name, email, docname): email = Email.objects.get(address=addr,person__isnull=True) except Email.DoesNotExist: # most likely we just need to create it - email = Email(address=addr, origin=docname) + email = Email(address=addr) email.active = active - email.person = person if email.time is None: email.time = datetime.datetime.now() + email.origin = docname email.save() return person, email From 874aad0322150919908f96eb4e9ae7391188b97d Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Tue, 8 May 2018 16:24:26 +0000 Subject: [PATCH 13/29] Added a consent field to the Person model. - Legacy-Id: 15142 --- ietf/person/migrations/0003_auto_20180504_1519.py | 10 ++++++++++ ietf/person/models.py | 1 + 2 files changed, 11 insertions(+) diff --git a/ietf/person/migrations/0003_auto_20180504_1519.py b/ietf/person/migrations/0003_auto_20180504_1519.py index 1bbedbbec..8fb4531bf 100644 --- a/ietf/person/migrations/0003_auto_20180504_1519.py +++ b/ietf/person/migrations/0003_auto_20180504_1519.py @@ -97,4 +97,14 @@ class Migration(migrations.Migration): name='person', field=ietf.utils.models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='person.Person'), ), + migrations.AddField( + model_name='historicalperson', + name='consent', + field=models.BooleanField(verbose_name=b'I hereby give my consent to the use of the personal details I have provided within the IETF Datatracker', null=True, default=None, ), + ), + migrations.AddField( + model_name='person', + name='consent', + field=models.BooleanField(verbose_name=b'I hereby give my consent to the use of the personal details I have provided within the IETF Datatracker', null=True, default=None, ), + ), ] diff --git a/ietf/person/models.py b/ietf/person/models.py index 674d6fc5f..83cc5de49 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -44,6 +44,7 @@ class Person(models.Model): photo = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) photo_thumb = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) name_from_draft = models.CharField("Full Name (from submission)", null=True, max_length=255, editable=False, help_text="Name as found in a draft submission.") + consent = models.BooleanField("I hereby give my consent to the use of the personal details I have provided (photo, bio, name, email) within the IETF Datatracker", null=True, default=None) def __unicode__(self): return self.plain_name() From 246c348f1ea40be4855f8ff76bc3b8de79844ac4 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Tue, 8 May 2018 16:26:01 +0000 Subject: [PATCH 14/29] Disallow profile changes without consent given. Together with previous commits this fixes issues #2505 and #2507. - Legacy-Id: 15143 --- ietf/ietfauth/forms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py index 918c8a239..758d8767f 100644 --- a/ietf/ietfauth/forms.py +++ b/ietf/ietfauth/forms.py @@ -135,6 +135,11 @@ def get_person_form(*args, **kwargs): prevent_system_name(name) return ascii_cleaner(name) + def clean_consent(self): + consent = self.cleaned_data.get('consent') + if consent == False: + raise forms.ValidationError("In order to modify your profile data, you must permit the IETF to use the uploaded data.") + return PersonForm(*args, **kwargs) From 70ed611472faf5e0442bef3e789630c60de93c6e Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Thu, 17 May 2018 16:45:21 +0000 Subject: [PATCH 15/29] Changed the field type for the Person.consent field. - Legacy-Id: 15146 --- ietf/ietfauth/forms.py | 3 +++ ietf/person/migrations/0003_auto_20180504_1519.py | 4 ++-- ietf/person/models.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py index 758d8767f..480471f79 100644 --- a/ietf/ietfauth/forms.py +++ b/ietf/ietfauth/forms.py @@ -94,6 +94,9 @@ def get_person_form(*args, **kwargs): class Meta: model = Person exclude = exclude_list + widgets = { + 'consent': forms.widgets.CheckboxInput, + } def __init__(self, *args, **kwargs): super(PersonForm, self).__init__(*args, **kwargs) diff --git a/ietf/person/migrations/0003_auto_20180504_1519.py b/ietf/person/migrations/0003_auto_20180504_1519.py index 8fb4531bf..7abd048e2 100644 --- a/ietf/person/migrations/0003_auto_20180504_1519.py +++ b/ietf/person/migrations/0003_auto_20180504_1519.py @@ -100,11 +100,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='historicalperson', name='consent', - field=models.BooleanField(verbose_name=b'I hereby give my consent to the use of the personal details I have provided within the IETF Datatracker', null=True, default=None, ), + field=models.NullBooleanField(default=None, verbose_name=b'I hereby give my consent to the use of the personal details I have provided (photo, bio, name, email) within the IETF Datatracker'), ), migrations.AddField( model_name='person', name='consent', - field=models.BooleanField(verbose_name=b'I hereby give my consent to the use of the personal details I have provided within the IETF Datatracker', null=True, default=None, ), + field=models.NullBooleanField(default=None, verbose_name=b'I hereby give my consent to the use of the personal details I have provided (photo, bio, name, email) within the IETF Datatracker'), ), ] diff --git a/ietf/person/models.py b/ietf/person/models.py index 83cc5de49..40b80c3f6 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -44,7 +44,7 @@ class Person(models.Model): photo = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) photo_thumb = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None) name_from_draft = models.CharField("Full Name (from submission)", null=True, max_length=255, editable=False, help_text="Name as found in a draft submission.") - consent = models.BooleanField("I hereby give my consent to the use of the personal details I have provided (photo, bio, name, email) within the IETF Datatracker", null=True, default=None) + consent = models.NullBooleanField("I hereby give my consent to the use of the personal details I have provided (photo, bio, name, email) within the IETF Datatracker", null=True, default=None) def __unicode__(self): return self.plain_name() From f47e1ff2ff381ba5303d6e4fdd3a81e000f906f6 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Thu, 17 May 2018 16:48:49 +0000 Subject: [PATCH 16/29] Updated email admin to show origin in lists. - Legacy-Id: 15147 --- ietf/person/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/person/admin.py b/ietf/person/admin.py index ed50eaeaa..864141cc1 100644 --- a/ietf/person/admin.py +++ b/ietf/person/admin.py @@ -5,7 +5,7 @@ from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent from ietf.person.name import name_parts class EmailAdmin(simple_history.admin.SimpleHistoryAdmin): - list_display = ["address", "person", "time", "active", ] + list_display = ["address", "person", "time", "active", "origin"] raw_id_fields = ["person", ] search_fields = ["address", "person__name", ] admin.site.register(Email, EmailAdmin) From 619b20d2e74f250211f973b8d8eaa571899fdbe2 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Thu, 17 May 2018 16:50:49 +0000 Subject: [PATCH 17/29] Data migration to assign email origin based on existing records (author, role, and more). - Legacy-Id: 15148 --- .../migrations/0004_populate_email_origin.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 ietf/person/migrations/0004_populate_email_origin.py diff --git a/ietf/person/migrations/0004_populate_email_origin.py b/ietf/person/migrations/0004_populate_email_origin.py new file mode 100644 index 000000000..393748180 --- /dev/null +++ b/ietf/person/migrations/0004_populate_email_origin.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-05-10 05:28 +from __future__ import unicode_literals + +import sys + +from django.db import migrations + +import debug + +def populate_email_origin(apps, schema_editor): + Submission = apps.get_model('submit', 'Submission') + Document = apps.get_model('doc', 'Document') + DocHistory = apps.get_model('doc', 'DocHistory') + DocumentAuthor = apps.get_model('doc', 'DocumentAuthor') + DocHistoryAuthor= apps.get_model('doc', 'DocHistoryAuthor') + Role = apps.get_model('group', 'Role') + RoleHistory = apps.get_model('group', 'RoleHistory') + ReviewRequest = apps.get_model('review', 'ReviewRequest') + LiaisonStatement= apps.get_model('liaisons', 'LiaisonStatement') + # + Email = apps.get_model('person', 'Email') + # + sys.stdout.write("\n") + # + sys.stdout.write("\n ** This migration may take some time. Expect at least a few minutes **.\n\n") + sys.stdout.write(" Initializing data structures...\n") + emails = dict([ (e.address, e) for e in Email.objects.filter(origin='') ]) + + count = 0 + sys.stdout.write(" Assigning email origins from Submission records...\n") + for o in Submission.objects.all().order_by('-submission_date'): + for a in o.authors: + addr = a['email'] + if addr in emails: + e = emails[addr] + if e.origin != o.name: + e.origin = "author: %s" % o.name + count += 1 + e.save() + del emails[addr] + sys.stdout.write(" Submission email origins assigned: %d\n" % count) + + for model in (DocumentAuthor, DocHistoryAuthor, ): + count = 0 + sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) + for o in model.objects.filter(email__origin=''): + if not o.email.origin: + o.email.origin = "author: %s" % o.document.name + o.email.save() + count += 1 + sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) + + for model in (Role, RoleHistory, ): + count = 0 + sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) + for o in model.objects.filter(email__origin=''): + if not o.email.origin: + o.email.origin = "role: %s %s" % (o.group.acronym, o.name.slug) + o.email.save() + count += 1 + sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) + + for model in (ReviewRequest, ): + count = 0 + sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) + for o in model.objects.filter(reviewer__origin=''): + if not o.reviewer.origin: + o.reviewer.origin = "reviewer: %s" % (o.doc.name) + o.reviewer.save() + count += 1 + sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) + + for model in (LiaisonStatement, ): + count = 0 + sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) + for o in model.objects.filter(from_contact__origin=''): + if not o.from_contact.origin: + o.from_contact.origin = "liaison: %s" % (','.join([ g.acronym for g in o.from_groups.all() ])) + o.from_contact.save() + count += 1 + sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) + + for model in (Document, DocHistory, ): + count = 0 + sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) + for o in model.objects.filter(shepherd__origin=''): + if not o.shepherd.origin: + o.shepherd.origin = "shepherd: %s" % o.name + o.shepherd.save() + count += 1 + sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) + + sys.stdout.write("\n") + sys.stdout.write(" Email records with origin indication: %d\n" % Email.objects.exclude(origin='').count()) + sys.stdout.write(" Email records without origin indication: %d\n" % Email.objects.filter(origin='').count()) + +def reverse(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0003_auto_20180504_1519'), + ] + + operations = [ + migrations.RunPython(populate_email_origin, reverse) + ] From 6c3ec5b18ed0b63d6ebb5b5454133ac41cc976d9 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Thu, 17 May 2018 16:56:26 +0000 Subject: [PATCH 18/29] Added Email origin to Email record creation throughout the codebase. - Legacy-Id: 15149 --- ietf/doc/views_draft.py | 3 +++ ietf/group/views.py | 7 +++++++ ietf/liaisons/forms.py | 9 +++++++++ ietf/nomcom/utils.py | 2 +- ietf/review/import_from_review_tool.py | 2 +- ietf/secr/areas/views.py | 4 ++++ ietf/secr/drafts/forms.py | 6 ++++++ ietf/secr/groups/forms.py | 3 +++ ietf/secr/groups/views.py | 4 ++++ ietf/secr/roles/views.py | 4 ++++ ietf/stats/utils.py | 2 +- ietf/submit/utils.py | 4 ++-- 12 files changed, 45 insertions(+), 5 deletions(-) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index f1d56ca18..38c9463d1 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -954,6 +954,9 @@ def edit_shepherd(request, name): events = [] doc.shepherd = form.cleaned_data['shepherd'] + if not doc.shepherd.origin: + doc.shepherd.origin = 'shepherd: %s' % doc.name + doc.shepherd.save() c = DocEvent(type="added_comment", doc=doc, rev=doc.rev, by=request.user.person) c.desc = "Document shepherd changed to "+ (doc.shepherd.person.name if doc.shepherd else "(None)") diff --git a/ietf/group/views.py b/ietf/group/views.py index 417adf1fd..54c9990e9 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -958,6 +958,10 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None): group.role_set.filter(name=slug).delete() for e in new: Role.objects.get_or_create(name_id=slug, email=e, group=group, person=e.person) + if not e.origin or e.origin == e.person.user.username: + e.origin = "role: %s %s" % (group.acronym, slug) + e.save() + added = set(new) - set(old) deleted = set(old) - set(new) if added: @@ -1206,6 +1210,9 @@ def stream_edit(request, acronym): group.role_set.filter(name=slug).delete() for e in new: Role.objects.get_or_create(name_id=slug, email=e, group=group, person=e.person) + if not e.origin or e.origin == e.person.user.username: + e.origin = "role: %s %s" % (group.acronym, slug) + e.save() return redirect("ietf.group.views.streams") else: diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py index 3acebfa09..14c59cfa6 100644 --- a/ietf/liaisons/forms.py +++ b/ietf/liaisons/forms.py @@ -500,6 +500,15 @@ class OutgoingLiaisonForm(LiaisonModelForm): if has_role(self.user, "Liaison Manager"): self.fields['to_groups'].initial = [queryset.first()] + def save(self, commit=False): + instance = super(EditModelForm, self).save(commit=False) + + if 'from_contact' in self.changed_data: + email = self.cleaned_data.get('from_contact') + if not email.origin: + email.origin = "liaison: %s" % (','.join([ g.acronym for g in instance.from_groups.all() ])) + email.save() + class EditLiaisonForm(LiaisonModelForm): def __init__(self, *args, **kwargs): super(EditLiaisonForm, self).__init__(*args, **kwargs) diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index d09436699..c51360e1e 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -372,7 +372,7 @@ def make_nomineeposition(nomcom, candidate, position, author): def make_nomineeposition_for_newperson(nomcom, candidate_name, candidate_email, position, author): # This is expected to fail if called with an existing email address - email = Email.objects.create(address=candidate_email, origin=nomcom.group.acronym) + email = Email.objects.create(address=candidate_email, origin="nominee: %s" % nomcom.group.acronym) person = Person.objects.create(name=candidate_name, ascii=unidecode_name(candidate_name), ) diff --git a/ietf/review/import_from_review_tool.py b/ietf/review/import_from_review_tool.py index a3059d3b0..4e1488d2c 100755 --- a/ietf/review/import_from_review_tool.py +++ b/ietf/review/import_from_review_tool.py @@ -101,7 +101,7 @@ with db_con.cursor() as c: for name in new_aliases: Alias.objects.create(person=person, name=name) - email, created = Email.objects.get_or_create(address=row.email, person=person, origin=__name__) + email, created = Email.objects.get_or_create(address=row.email, person=person, origin="import: %s" % __name__) if created: print "created email", email diff --git a/ietf/secr/areas/views.py b/ietf/secr/areas/views.py index 1099303ce..ca0bcb0a8 100644 --- a/ietf/secr/areas/views.py +++ b/ietf/secr/areas/views.py @@ -214,6 +214,10 @@ def people(request, name): # create role Role.objects.create(name_id='pre-ad',group=area,email=email,person=person) + if not email.origin or email.origin == person.user.username: + email.origin = "role: %s %s" % (area.acronym, 'pre-ad') + email.save() + messages.success(request, 'New Area Director added successfully!') return redirect('ietf.secr.areas.views.view', name=name) else: diff --git a/ietf/secr/drafts/forms.py b/ietf/secr/drafts/forms.py index 620f9bbbb..c12321814 100644 --- a/ietf/secr/drafts/forms.py +++ b/ietf/secr/drafts/forms.py @@ -178,6 +178,12 @@ class EditModelForm(forms.ModelForm): else: m.tags.remove('rfc-rev') + if 'shepherd' in self.changed_data: + email = self.cleaned_data.get('shepherd') + if not email.origin: + email.origin = 'shepherd: %s' % m.name + email.save() + # handle replaced by return m diff --git a/ietf/secr/groups/forms.py b/ietf/secr/groups/forms.py index 0d1d72cb5..c002b3b30 100644 --- a/ietf/secr/groups/forms.py +++ b/ietf/secr/groups/forms.py @@ -193,6 +193,9 @@ class RoleForm(forms.Form): name = cleaned_data['name'] group_acronym = cleaned_data['group_acronym'] + if email.person != person: + raise forms.ValidationError('ERROR: The person associated with the chosen email address is different from the chosen person') + if Role.objects.filter(name=name,group=self.group,person=person,email=email): raise forms.ValidationError('ERROR: This is a duplicate entry') diff --git a/ietf/secr/groups/views.py b/ietf/secr/groups/views.py index 956978853..6c0d46cb3 100644 --- a/ietf/secr/groups/views.py +++ b/ietf/secr/groups/views.py @@ -347,6 +347,10 @@ def people(request, acronym): email=email, group=group) + if not email.origin or email.origin == person.user.username: + email.origin = "role: %s %s" % (group.acronym, name.slug) + email.save() + messages.success(request, 'New %s added successfully!' % name) return redirect('ietf.secr.groups.views.people', acronym=group.acronym) else: diff --git a/ietf/secr/roles/views.py b/ietf/secr/roles/views.py index cd53b55e8..1063b9271 100644 --- a/ietf/secr/roles/views.py +++ b/ietf/secr/roles/views.py @@ -93,6 +93,10 @@ def main(request): email=email, group=group) + if not email.origin or email.origin == person.user.username: + email.origin = "role: %s %s" % (group.acronym, name.slug) + email.save() + messages.success(request, 'New %s added successfully!' % name) url = reverse('ietf.secr.roles.views.main') + '?group=%s' % group.acronym return HttpResponseRedirect(url) diff --git a/ietf/stats/utils.py b/ietf/stats/utils.py index c9a473aee..a7ecc0065 100644 --- a/ietf/stats/utils.py +++ b/ietf/stats/utils.py @@ -306,7 +306,7 @@ def get_meeting_registration_data(meeting): try: email = Email.objects.get(person=person, address=address[:64]) except Email.DoesNotExist: - email = Email.objects.create(person=person, address=address[:64], origin='ietf %s registration'%meeting.number) + email = Email.objects.create(person=person, address=address[:64], origin='registration: ietf-%s'%meeting.number) if email.address != address: debug.say("Truncated address: %s --> %s" % (address, email.address)) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 836fb6d36..314ff9854 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -452,7 +452,7 @@ def ensure_person_email_info_exists(name, email, docname): try: email = person.email_set.get(address=addr) - email.origin = docname # overwrite earlier origin + email.origin = "author: %s" % docname # overwrite earlier origin email.save() except Email.DoesNotExist: try: @@ -468,7 +468,7 @@ def ensure_person_email_info_exists(name, email, docname): email.person = person if email.time is None: email.time = datetime.datetime.now() - email.origin = docname + email.origin = "author: %s" % docname email.save() return person, email From 2875c66ce31323250f0ffc5cf211efb80013b792 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Thu, 17 May 2018 17:01:22 +0000 Subject: [PATCH 19/29] Added another category of personal information to the personal-information page, after review of personal information in the code. Completes issue #2501. - Legacy-Id: 15150 --- ietf/templates/help/personal-information.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ietf/templates/help/personal-information.html b/ietf/templates/help/personal-information.html index 32f1d00f2..5c02d2588 100644 --- a/ietf/templates/help/personal-information.html +++ b/ietf/templates/help/personal-information.html @@ -76,6 +76,7 @@ <li>Personal biography;</li> <li>Personal email addresses not derived from IETF contributions; and </li> <li>Personal account login information</li> + <li>Personal notification subscriptions</li> </ul> <p> From 6ec050d807ee4a9a563e879cbd4c402af394116a Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Thu, 24 May 2018 16:11:05 +0000 Subject: [PATCH 20/29] Removed a long-running data migration (it will return in the following release). - Legacy-Id: 15171 --- .../migrations/0004_populate_email_origin.py | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 ietf/person/migrations/0004_populate_email_origin.py diff --git a/ietf/person/migrations/0004_populate_email_origin.py b/ietf/person/migrations/0004_populate_email_origin.py deleted file mode 100644 index 393748180..000000000 --- a/ietf/person/migrations/0004_populate_email_origin.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.12 on 2018-05-10 05:28 -from __future__ import unicode_literals - -import sys - -from django.db import migrations - -import debug - -def populate_email_origin(apps, schema_editor): - Submission = apps.get_model('submit', 'Submission') - Document = apps.get_model('doc', 'Document') - DocHistory = apps.get_model('doc', 'DocHistory') - DocumentAuthor = apps.get_model('doc', 'DocumentAuthor') - DocHistoryAuthor= apps.get_model('doc', 'DocHistoryAuthor') - Role = apps.get_model('group', 'Role') - RoleHistory = apps.get_model('group', 'RoleHistory') - ReviewRequest = apps.get_model('review', 'ReviewRequest') - LiaisonStatement= apps.get_model('liaisons', 'LiaisonStatement') - # - Email = apps.get_model('person', 'Email') - # - sys.stdout.write("\n") - # - sys.stdout.write("\n ** This migration may take some time. Expect at least a few minutes **.\n\n") - sys.stdout.write(" Initializing data structures...\n") - emails = dict([ (e.address, e) for e in Email.objects.filter(origin='') ]) - - count = 0 - sys.stdout.write(" Assigning email origins from Submission records...\n") - for o in Submission.objects.all().order_by('-submission_date'): - for a in o.authors: - addr = a['email'] - if addr in emails: - e = emails[addr] - if e.origin != o.name: - e.origin = "author: %s" % o.name - count += 1 - e.save() - del emails[addr] - sys.stdout.write(" Submission email origins assigned: %d\n" % count) - - for model in (DocumentAuthor, DocHistoryAuthor, ): - count = 0 - sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) - for o in model.objects.filter(email__origin=''): - if not o.email.origin: - o.email.origin = "author: %s" % o.document.name - o.email.save() - count += 1 - sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) - - for model in (Role, RoleHistory, ): - count = 0 - sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) - for o in model.objects.filter(email__origin=''): - if not o.email.origin: - o.email.origin = "role: %s %s" % (o.group.acronym, o.name.slug) - o.email.save() - count += 1 - sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) - - for model in (ReviewRequest, ): - count = 0 - sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) - for o in model.objects.filter(reviewer__origin=''): - if not o.reviewer.origin: - o.reviewer.origin = "reviewer: %s" % (o.doc.name) - o.reviewer.save() - count += 1 - sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) - - for model in (LiaisonStatement, ): - count = 0 - sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) - for o in model.objects.filter(from_contact__origin=''): - if not o.from_contact.origin: - o.from_contact.origin = "liaison: %s" % (','.join([ g.acronym for g in o.from_groups.all() ])) - o.from_contact.save() - count += 1 - sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) - - for model in (Document, DocHistory, ): - count = 0 - sys.stdout.write(" Assigning email origins from %s records...\n" % model.__name__) - for o in model.objects.filter(shepherd__origin=''): - if not o.shepherd.origin: - o.shepherd.origin = "shepherd: %s" % o.name - o.shepherd.save() - count += 1 - sys.stdout.write(" %s email origins assigned: %d\n" % (model.__name__, count)) - - sys.stdout.write("\n") - sys.stdout.write(" Email records with origin indication: %d\n" % Email.objects.exclude(origin='').count()) - sys.stdout.write(" Email records without origin indication: %d\n" % Email.objects.filter(origin='').count()) - -def reverse(apps, schema_editor): - pass - -class Migration(migrations.Migration): - - dependencies = [ - ('person', '0003_auto_20180504_1519'), - ] - - operations = [ - migrations.RunPython(populate_email_origin, reverse) - ] From 2522082979351736049ebd712029fed0542bd364 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:32:20 +0000 Subject: [PATCH 21/29] Changed the email origin field during test to hold user.username in order to exercise more of the code. Changed the EmailFactory to also use user.username as origin. - Legacy-Id: 15172 --- ietf/doc/tests_draft.py | 18 +++++++++--------- ietf/ietfauth/tests.py | 9 +++++---- .../management/commands/make_dummy_nomcom.py | 6 +++--- ietf/person/factories.py | 4 ++-- ietf/person/tests.py | 6 +++--- ietf/utils/test_data.py | 12 ++++++------ 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index cb2e36200..a25954d14 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -1001,7 +1001,7 @@ class IndividualInfoFormsTests(TestCase): doc.shepherd = Email.objects.get(person__user__username="plain") doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_shepherd", by=Person.objects.get(user__username="secretary"), desc="Test")]) - new_email = Email.objects.create(address="anotheremail@example.com", person=doc.shepherd.person, origin='test') + new_email = Email.objects.create(address="anotheremail@example.com", person=doc.shepherd.person, origin=doc.shepherd.person.user.username) r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -1435,8 +1435,8 @@ class ChangeReplacesTests(TestCase): expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), group=mars_wg, ) - p = Person.objects.create(name="basea_author") - e = Email.objects.create(address="basea_author@example.com", person=p, origin='test') + p = PersonFactory(name=u"basea_author") + e = Email.objects.create(address="basea_author@example.com", person=p, origin=p.user.username) self.basea.documentauthor_set.create(person=p, email=e, order=1) self.baseb = Document.objects.create( @@ -1448,8 +1448,8 @@ class ChangeReplacesTests(TestCase): expires=datetime.datetime.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), group=mars_wg, ) - p = Person.objects.create(name="baseb_author") - e = Email.objects.create(address="baseb_author@example.com", person=p, origin='test') + p = PersonFactory(name=u"baseb_author") + e = Email.objects.create(address="baseb_author@example.com", person=p, origin=p.user.username) self.baseb.documentauthor_set.create(person=p, email=e, order=1) self.replacea = Document.objects.create( @@ -1461,8 +1461,8 @@ class ChangeReplacesTests(TestCase): expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), group=mars_wg, ) - p = Person.objects.create(name="replacea_author") - e = Email.objects.create(address="replacea_author@example.com", person=p, origin='test') + p = PersonFactory(name=u"replacea_author") + e = Email.objects.create(address="replacea_author@example.com", person=p, origin=p.user.username) self.replacea.documentauthor_set.create(person=p, email=e, order=1) self.replaceboth = Document.objects.create( @@ -1474,8 +1474,8 @@ class ChangeReplacesTests(TestCase): expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), group=mars_wg, ) - p = Person.objects.create(name="replaceboth_author") - e = Email.objects.create(address="replaceboth_author@example.com", person=p, origin='test') + p = PersonFactory(name=u"replaceboth_author") + e = Email.objects.create(address="replaceboth_author@example.com", person=p, origin=p.user.username) self.replaceboth.documentauthor_set.create(person=p, email=e, order=1) self.basea.set_state(State.objects.get(used=True, type="draft", slug="active")) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 7d255df99..8380deb68 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -209,6 +209,7 @@ class IetfAuthTests(TestCase): "ascii_short": u"T. Name", "affiliation": "Test Org", "active_emails": email_address, + "consent": True, } # edit details - faulty ASCII @@ -308,7 +309,7 @@ class IetfAuthTests(TestCase): user.set_password("forgotten") user.save() p = Person.objects.create(name="Some One", ascii="Some One", user=user) - Email.objects.create(address=user.username, person=p, origin='test') + Email.objects.create(address=user.username, person=p, origin=user.username) # get r = self.client.get(url) @@ -418,7 +419,7 @@ class IetfAuthTests(TestCase): user.set_password("password") user.save() p = Person.objects.create(name="Some One", ascii="Some One", user=user) - Email.objects.create(address=user.username, person=p, origin='test') + Email.objects.create(address=user.username, person=p, origin=user.username) # log in r = self.client.post(redir_url, {"username":user.username, "password":"password"}) @@ -465,8 +466,8 @@ class IetfAuthTests(TestCase): user.set_password("password") user.save() p = Person.objects.create(name="Some One", ascii="Some One", user=user) - Email.objects.create(address=user.username, person=p, origin='test') - Email.objects.create(address="othername@example.org", person=p, origin='test') + Email.objects.create(address=user.username, person=p, origin=user.username) + Email.objects.create(address="othername@example.org", person=p, origin=user.username) # log in r = self.client.post(redir_url, {"username":user.username, "password":"password"}) diff --git a/ietf/nomcom/management/commands/make_dummy_nomcom.py b/ietf/nomcom/management/commands/make_dummy_nomcom.py index 6adb62ddf..a6724a326 100644 --- a/ietf/nomcom/management/commands/make_dummy_nomcom.py +++ b/ietf/nomcom/management/commands/make_dummy_nomcom.py @@ -39,18 +39,18 @@ class Command(BaseCommand): populate_personnel=False, populate_positions=False)) - e = EmailFactory(person__name=u'Dummy Chair', address=u'dummychair@example.com', person__user__username=u'dummychair', person__default_emails=False, origin='test') + e = EmailFactory(person__name=u'Dummy Chair', address=u'dummychair@example.com', person__user__username=u'dummychair', person__default_emails=False, origin='dummychair') e.person.user.set_password('password') e.person.user.save() nc.group.role_set.create(name_id=u'chair',person=e.person,email=e) - e = EmailFactory(person__name=u'Dummy Member', address=u'dummymember@example.com', person__user__username=u'dummymember', person__default_emails=False, origin='test') + e = EmailFactory(person__name=u'Dummy Member', address=u'dummymember@example.com', person__user__username=u'dummymember', person__default_emails=False, origin='dummymember') e.person.user.set_password('password') e.person.user.save() nc.group.role_set.create(name_id=u'member',person=e.person,email=e) - e = EmailFactory(person__name=u'Dummy Candidate', address=u'dummycandidate@example.com', person__user__username=u'dummycandidate', person__default_emails=False, origin='test') + e = EmailFactory(person__name=u'Dummy Candidate', address=u'dummycandidate@example.com', person__user__username=u'dummycandidate', person__default_emails=False, origin='dummycandidate') e.person.user.set_password('password') e.person.user.save() NomineePositionFactory(nominee__nomcom=nc, nominee__person=e.person, diff --git a/ietf/person/factories.py b/ietf/person/factories.py index 95793c100..0c7888185 100644 --- a/ietf/person/factories.py +++ b/ietf/person/factories.py @@ -71,7 +71,7 @@ class PersonFactory(factory.DjangoModelFactory): extracted = True if create and extracted: make_email = getattr(EmailFactory, 'create' if create else 'build') - make_email(person=obj, address=obj.user.email, origin='test') + make_email(person=obj, address=obj.user.email) @factory.post_generation def default_photo(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument @@ -131,4 +131,4 @@ class EmailFactory(factory.DjangoModelFactory): active = True primary = False - origin = '' + origin = factory.LazyAttribute(lambda obj: obj.person.user.username if obj.person.user else '') diff --git a/ietf/person/tests.py b/ietf/person/tests.py index 690712f7a..7fd961c17 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -42,9 +42,9 @@ class PersonTests(TestCase): def test_default_email(self): person = PersonFactory() - primary = EmailFactory(person=person, primary=True, active=True, origin='test') - EmailFactory(person=person, primary=False, active=True, origin='test') - EmailFactory(person=person, primary=False, active=False, origin='test') + primary = EmailFactory(person=person, primary=True, active=True) + EmailFactory(person=person, primary=False, active=True) + EmailFactory(person=person, primary=False, active=False) self.assertTrue(primary.address in person.formatted_email()) def test_profile(self): diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index de3a48be5..072e3ffd5 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -38,7 +38,7 @@ def create_person(group, role_name, name=None, username=None, email_address=None user.set_password(password) user.save() person = Person.objects.create(name=name, ascii=unidecode_name(smart_text(name)), user=user) - email = Email.objects.create(address=email_address, person=person, origin='test') + email = Email.objects.create(address=email_address, person=person, origin=user.username) Role.objects.create(group=group, name_id=role_name, person=person, email=email) return person @@ -112,7 +112,7 @@ def make_immutable_base_data(): for i in range(1, 10): u = User.objects.create(username="ad%s" % i) p = Person.objects.create(name="Ad No%s" % i, ascii="Ad No%s" % i, user=u) - email = Email.objects.create(address="ad%s@ietf.org" % i, person=p, origin='test') + email = Email.objects.create(address="ad%s@ietf.org" % i, person=p, origin=u.username) if i < 6: # active Role.objects.create(name_id="ad", group=area, person=p, email=email) @@ -232,7 +232,7 @@ def make_test_data(): u.set_password("plain+password") u.save() plainman = Person.objects.create(name="Plain Man", ascii="Plain Man", user=u) - email = Email.objects.create(address="plain@example.com", person=plainman, origin='test') + email = Email.objects.create(address="plain@example.com", person=plainman, origin=u.username) # group personnel create_person(mars_wg, "chair", name="WG Cháir Man", username="marschairman") @@ -473,7 +473,7 @@ def make_review_data(doc): u.set_password("reviewer+password") u.save() reviewer = Person.objects.create(name=u"Some Réviewer", ascii="Some Reviewer", user=u) - email = Email.objects.create(address="reviewer@example.com", person=reviewer, origin='test') + email = Email.objects.create(address="reviewer@example.com", person=reviewer, origin=u.username) for team in (team1, team2, team3): Role.objects.create(name_id="reviewer", person=reviewer, email=email, group=team) @@ -496,14 +496,14 @@ def make_review_data(doc): u.set_password("reviewsecretary+password") u.save() reviewsecretary = Person.objects.create(name=u"Réview Secretary", ascii="Review Secretary", user=u) - reviewsecretary_email = Email.objects.create(address="reviewsecretary@example.com", person=reviewsecretary, origin='test') + reviewsecretary_email = Email.objects.create(address="reviewsecretary@example.com", person=reviewsecretary, origin=u.username) Role.objects.create(name_id="secr", person=reviewsecretary, email=reviewsecretary_email, group=team1) u = User.objects.create(username="reviewsecretary3") u.set_password("reviewsecretary3+password") u.save() reviewsecretary3 = Person.objects.create(name=u"Réview Secretary3", ascii="Review Secretary3", user=u) - reviewsecretary3_email = Email.objects.create(address="reviewsecretary3@example.com", person=reviewsecretary, origin='test') + reviewsecretary3_email = Email.objects.create(address="reviewsecretary3@example.com", person=reviewsecretary, origin=u.username) Role.objects.create(name_id="secr", person=reviewsecretary3, email=reviewsecretary3_email, group=team3) return review_req From 81e78c70a023bf0dbb8662af53cf764f49bc08b7 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:34:27 +0000 Subject: [PATCH 22/29] Added guards against asking for properties on None in a couple of places. - Legacy-Id: 15173 --- ietf/doc/views_draft.py | 2 +- ietf/secr/drafts/forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 38c9463d1..fdaea6713 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -954,7 +954,7 @@ def edit_shepherd(request, name): events = [] doc.shepherd = form.cleaned_data['shepherd'] - if not doc.shepherd.origin: + if doc.shepherd and not doc.shepherd.origin: doc.shepherd.origin = 'shepherd: %s' % doc.name doc.shepherd.save() diff --git a/ietf/secr/drafts/forms.py b/ietf/secr/drafts/forms.py index c12321814..db7af4d39 100644 --- a/ietf/secr/drafts/forms.py +++ b/ietf/secr/drafts/forms.py @@ -180,7 +180,7 @@ class EditModelForm(forms.ModelForm): if 'shepherd' in self.changed_data: email = self.cleaned_data.get('shepherd') - if not email.origin: + if email and not email.origin: email.origin = 'shepherd: %s' % m.name email.save() From f6537fda5921eae871de472031909b2eba8b6d65 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:36:06 +0000 Subject: [PATCH 23/29] Added a dagger at the end of some fields in the account data forms to signify consent-based fields, and made the consent field required. - Legacy-Id: 15174 --- ietf/ietfauth/forms.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py index 480471f79..07e78f077 100644 --- a/ietf/ietfauth/forms.py +++ b/ietf/ietfauth/forms.py @@ -1,3 +1,7 @@ +# Copyright The IETF Trust 2016, All Rights Reserved +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function + import re from unidecode import unidecode @@ -108,6 +112,12 @@ def get_person_form(*args, **kwargs): if self.initial.get("ascii") == self.initial.get("name"): self.initial["ascii"] = "" + for f in ['name', 'ascii', 'ascii_short', 'biography', 'photo', 'photo_thumb', ]: + if f in self.fields: + self.fields[f].label += ' \u2020' + + self.fields["consent"].required = True + self.unidecoded_ascii = False if self.data and not self.data.get("ascii", "").strip(): From 2fd1f81749c28bf6d9baba5b5cfdc77c9bca1667 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:36:50 +0000 Subject: [PATCH 24/29] Added assignment of email origin in another place. - Legacy-Id: 15175 --- ietf/ietfauth/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index 56013119a..c71f44f35 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -251,6 +251,8 @@ def profile(request): email.primary = email.address == primary_email if email.primary and not email.active: email.active = True + if not email.origin: + email.origin = person.user.username email.save() # Make sure the alias table contains any new and/or old names. From c97f6376a32f9b1d0115a029db786c4d266b1f80 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:38:40 +0000 Subject: [PATCH 25/29] Simplified the email.origin assignment code for outgoing liaisons. - Legacy-Id: 15176 --- ietf/liaisons/forms.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py index 14c59cfa6..2eb16eb07 100644 --- a/ietf/liaisons/forms.py +++ b/ietf/liaisons/forms.py @@ -266,8 +266,12 @@ class LiaisonModelForm(BetterModelForm): def clean_from_contact(self): contact = self.cleaned_data.get('from_contact') + from_groups = self.cleaned_data.get('from_groups') try: email = Email.objects.get(address=contact) + if not email.origin: + email.origin = "liaison: %s" % (','.join([ g.acronym for g in from_groups.all() ])) + email.save() except ObjectDoesNotExist: raise forms.ValidationError('Email address does not exist') return email @@ -500,14 +504,6 @@ class OutgoingLiaisonForm(LiaisonModelForm): if has_role(self.user, "Liaison Manager"): self.fields['to_groups'].initial = [queryset.first()] - def save(self, commit=False): - instance = super(EditModelForm, self).save(commit=False) - - if 'from_contact' in self.changed_data: - email = self.cleaned_data.get('from_contact') - if not email.origin: - email.origin = "liaison: %s" % (','.join([ g.acronym for g in instance.from_groups.all() ])) - email.save() class EditLiaisonForm(LiaisonModelForm): def __init__(self, *args, **kwargs): From aa1e42100b782060cc7a1b5344661a6142cba8ed Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:39:28 +0000 Subject: [PATCH 26/29] Fixed a long-standing bug in the liaison.name() code. - Legacy-Id: 15177 --- ietf/liaisons/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/liaisons/models.py b/ietf/liaisons/models.py index 37c24d230..b42d72044 100644 --- a/ietf/liaisons/models.py +++ b/ietf/liaisons/models.py @@ -84,11 +84,11 @@ class LiaisonStatement(models.Model): if self.from_groups.count(): frm = ', '.join([i.acronym or i.name for i in self.from_groups.all()]) else: - frm = self.from_name + frm = self.from_contact.person.name if self.to_groups.count(): to = ', '.join([i.acronym or i.name for i in self.to_groups.all()]) else: - to = self.to_name + to = self.to_contacts return slugify("liaison" + " " + self.submitted.strftime("%Y-%m-%d") + " " + frm[:50] + " " + to[:50] + " " + self.title[:115]) @property From b1440e818bae8a7cd45413707b158d38f3e38f1f Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:40:58 +0000 Subject: [PATCH 27/29] Added assingment of the person.name_from_draft field on draft submission. To be used to replace the content of person.name if someone requires removal of consent-based name info. - Legacy-Id: 15178 --- ietf/submit/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 314ff9854..192b3401e 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -437,9 +437,12 @@ def ensure_person_email_info_exists(name, email, docname): if not person: person = Person() person.name = name + person.name_from_draft = name log.assertion('isinstance(person.name, six.text_type)') person.ascii = unidecode_name(person.name).decode('ascii') person.save() + else: + person.name_from_draft = name # make sure we have an email address if addr and (addr.startswith('unknown-email-') or is_valid_email(addr)): From 08c137c960b62e0fdf040ceb4663be7ed165ef1c Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Sat, 26 May 2018 08:42:36 +0000 Subject: [PATCH 28/29] Updated the edit_profile template with information about consent-based fields. Fixes issue #2502. - Legacy-Id: 15179 --- ietf/templates/registration/edit_profile.html | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/ietf/templates/registration/edit_profile.html b/ietf/templates/registration/edit_profile.html index 16dcece35..8c5079ce0 100644 --- a/ietf/templates/registration/edit_profile.html +++ b/ietf/templates/registration/edit_profile.html @@ -12,22 +12,57 @@ {% origin %} <h1>Profile for {{ user.username }}</h1> + <p> + Personal information in the datatracker which is derived from your contributions + to the IETF standards development process is covered by the EU General Data Protection + Regulation's + <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1">Article 6(1) (f)</a> + covering IETF's Legitimate Interest due to the IETF's mission of developing standards + for the internet. See also the page on <a href="/help/personal-information">handling + of personal information</a>. + + </p> + <p> + + Personal information which is <b>not</b> derived from your contributions is covered by the EU + <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1">GDPR Article 6(1) (a)</a> + regarding consent. All such information is visible on this page, and is shown with the + dagger symbol † next to it. Most of this + information can be edited or removed on this page. There are some exceptions, such + as photos, which currently require an email to <a href="mailto:ietf-action@ietf.org">the Secretariat</a> + if you wish to update or remove the information. + + </p> + <hr> + <form class="form-horizontal" method="post"> {% csrf_token %} - {% bootstrap_form_errors person_form 'non_fields' %} + {% bootstrap_form_errors person_form %} + {% for f in new_email_forms %} + {% bootstrap_form_errors f %} + {% endfor %} + <div class="form-group"> - <label class="col-sm-2 control-label">User name</label> + <label class="col-sm-2 control-label">User name †</label> <div class="col-sm-10"> - <p class="form-control-static">{{ user.username }}</p> + <p class="form-control-static"> + {{ user.username }} + <a href="/accounts/username/"><span class="fa fa-pencil"></span></a> + </p> + </div> </div> <div class="form-group"> - <label class="col-sm-2 control-label">Password</label> + <label class="col-sm-2 control-label">Password †</label> <div class="col-sm-10"> - <p class="form-control-static"><a href="{% url 'ietf.ietfauth.views.change_password' %}">Password change form</a></p> + <p class="form-control-static"> + + <a href="/accounts/password/">Password change form</a> + <a href="/accounts/password/"><span class="fa fa-pencil"></span></a> + </p> </div> </div> @@ -78,7 +113,12 @@ {% bootstrap_field role.email_form.email layout="horizontal" show_label=False %} {% endfor %} - {% bootstrap_form person_form layout="horizontal" %} + {% bootstrap_field person_form.name layout="horizontal" %} + {% bootstrap_field person_form.ascii layout="horizontal" %} + {% if roles %} + {% bootstrap_field person_form.biography layout="horizontal" %} + {% endif %} + {% bootstrap_field person_form.consent layout="horizontal" %} <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> From 46bee81bdc4906a16cbe907b2509e924159ed9de Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz <henrik@levkowetz.com> Date: Mon, 28 May 2018 09:48:27 +0000 Subject: [PATCH 29/29] Fixed a test email object creation issue. - Legacy-Id: 15180 --- ietf/nomcom/test_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ietf/nomcom/test_data.py b/ietf/nomcom/test_data.py index 70dad80f2..c04621c91 100644 --- a/ietf/nomcom/test_data.py +++ b/ietf/nomcom/test_data.py @@ -123,7 +123,9 @@ def nomcom_test_data(): u.set_password(COMMUNITY_USER+"+password") u.save() plainman, _ = Person.objects.get_or_create(name="Plain Man", ascii="Plain Man", user=u) - email, _ = Email.objects.get_or_create(address="plain@example.com", person=plainman, origin='test') + email = Email.objects.filter(address="plain@example.com", person=plainman).first() + if not email: + email = Email.objects.create(address="plain@example.com", person=plainman, origin=u.username) nominee, _ = Nominee.objects.get_or_create(email=email, nomcom=nomcom) # positions