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)&nbsp;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)&nbsp;f) of <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1">
+     6(1)&nbsp;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)&nbsp;(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)&nbsp;(a)</a>
+      regarding consent.  All such information is visible on this page, and is shown with the
+      dagger symbol &dagger; 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 &dagger;</label>
       <div class="col-sm-10">
-        <p class="form-control-static">{{ user.username }}</p>
+	<p class="form-control-static">
+	  {{ user.username }}
+	  &nbsp;<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 &dagger;</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>
+	   &nbsp;<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