From 4e8ef492fcea76c4dec651eeda83e66cde09a1d7 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Fri, 18 Jan 2019 18:27:29 +0000
Subject: [PATCH] Modified the rolodex code to make sure we capture information
 about the origin of email addresses if they are added by the secretariat, to
 ensure GPR compliance.  - Legacy-Id: 15898

---
 .../migrations/0009_auto_20190118_0725.py     | 25 ++++++++++
 ietf/person/models.py                         |  2 +-
 ietf/secr/rolodex/forms.py                    | 46 +++++++++++++++++--
 ietf/secr/rolodex/tests.py                    | 13 +++++-
 ietf/secr/static/secr/css/custom.css          |  4 +-
 5 files changed, 82 insertions(+), 8 deletions(-)
 create mode 100644 ietf/person/migrations/0009_auto_20190118_0725.py

diff --git a/ietf/person/migrations/0009_auto_20190118_0725.py b/ietf/person/migrations/0009_auto_20190118_0725.py
new file mode 100644
index 000000000..e3a1e4233
--- /dev/null
+++ b/ietf/person/migrations/0009_auto_20190118_0725.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2019-01-18 07:25
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('person', '0008_auto_20181014_1448'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='email',
+            name='origin',
+            field=models.CharField(help_text=b"The origin of the address: the user's email address, or 'author: DRAFTNAME' if a draft, or 'role: GROUP/ROLE' if a role.", max_length=150),
+        ),
+        migrations.AlterField(
+            model_name='historicalemail',
+            name='origin',
+            field=models.CharField(help_text=b"The origin of the address: the user's email address, or 'author: DRAFTNAME' if a draft, or 'role: GROUP/ROLE' if a role.", max_length=150),
+        ),
+    ]
diff --git a/ietf/person/models.py b/ietf/person/models.py
index 68edc683a..d0159c69c 100644
--- a/ietf/person/models.py
+++ b/ietf/person/models.py
@@ -258,7 +258,7 @@ class Email(models.Model):
     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
+    origin = models.CharField(max_length=150, blank=False, help_text="The origin of the address: the user's email address, or 'author: DRAFTNAME' if a draft, or 'role: GROUP/ROLE' if a role.")       # User.username or Document.name
     active = models.BooleanField(default=True)      # Old email addresses are *not* purged, as history
                                                     # information points to persons through these
 
diff --git a/ietf/secr/rolodex/forms.py b/ietf/secr/rolodex/forms.py
index b6f17e0dd..eafcd5147 100644
--- a/ietf/secr/rolodex/forms.py
+++ b/ietf/secr/rolodex/forms.py
@@ -1,12 +1,22 @@
+# Copyright The IETF Trust 2016, All Rights Reserved
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+
+import re
+
 from django import forms
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.core.validators import validate_email
+from django.core.validators import validate_email, EmailValidator
 
+import debug                            # pyflakes:ignore
+
+from ietf.doc.models import Document
+from ietf.group.models import Group
+from ietf.name.models import RoleName
 from ietf.person.models import Email, Person
 
-import re
 
 class SearchForm(forms.Form):
     name = forms.CharField(max_length=50,required=False)
@@ -27,7 +37,37 @@ class EmailForm(forms.ModelForm):
     class Meta:
         model = Email
         fields = '__all__'
-        widgets = {'address': forms.TextInput(attrs={'readonly':True})}
+        widgets = {
+            'address': forms.TextInput(attrs={'readonly':True}),
+            'origin':  forms.TextInput(attrs={'blank':False}),
+        }
+
+    def clean_origin(self):
+        validate_email = EmailValidator("Please provide the origin of the new email: A valid user email if provided by email, or 'author: doc' or 'role: role spec'.")
+        if 'origin' in self.changed_data and self.instance.origin:
+            raise forms.ValidationError("You may not change existing origin fields, only set the value when empty")
+        origin = self.cleaned_data['origin']
+        if ':' in origin:
+            valid_tags = ['author', 'role', 'registration', ]
+            tag, value = [ v.strip() for v in origin.split(':', 1) ]
+            if not tag in valid_tags:
+                raise forms.ValidationError("Invalid tag.  Valid tags are: %s" % ','.join(valid_tags))
+            if   tag == 'author':
+                if not Document.objects.filter(name=value).exists():
+                    raise forms.ValidationError("Invalid document: %s. A valid document is required with 'author:'" % value)
+            elif tag == 'role':
+                if not ' ' in value:
+                    raise forms.ValidationError("Invalid role spec: %s.  Please indicate 'group role'." % value)
+                acronym, slug = value.split(None, 1)
+                if not Group.objects.filter(acronym=acronym).exists():
+                    raise forms.ValidationError("Invalid group: %s. A valid 'group role' string is required with 'role:'" % acronym)
+                if not RoleName.objects.filter(slug=slug).exists():
+                    roles = RoleName.objects.values_list('slug', flat=True)
+                    raise forms.ValidationError("Invalid role: %s. A valid 'group role' string is required with 'role:'.\n  Valid roles are: %s" % (slug, ', '.join(roles)))
+        else:
+            validate_email(origin)
+        return origin
+                
 
 class EditPersonForm(forms.ModelForm):
     class Meta:
diff --git a/ietf/secr/rolodex/tests.py b/ietf/secr/rolodex/tests.py
index 3cd897127..93e0e9e2f 100644
--- a/ietf/secr/rolodex/tests.py
+++ b/ietf/secr/rolodex/tests.py
@@ -3,6 +3,7 @@ from django.urls import reverse
 import debug                            # pyflakes:ignore
 
 from ietf.utils.test_utils import TestCase
+from ietf.group.factories import GroupFactory, RoleFactory
 from ietf.person.factories import PersonFactory, UserFactory
 from ietf.person.models import Person, User
 
@@ -47,11 +48,15 @@ class RolodexTestCase(TestCase):
 
     def test_edit_replace_user(self):
         person = PersonFactory()
+        email = person.email()
         user = UserFactory()
+        group = GroupFactory(type_id='wg')
+        role = RoleFactory(group=group,name_id='chair',person=person)
         url = reverse('ietf.secr.rolodex.views.edit', kwargs={'id':person.id})
         redirect_url = reverse('ietf.secr.rolodex.views.view', kwargs={'id':person.id})
         self.client.login(username="secretary", password="secretary+password")
         response = self.client.get(url)
+        #debug.show('unicontent(response)')
         self.assertEqual(response.status_code, 200)
         post_data = {
             'name': person.name,
@@ -59,8 +64,12 @@ class RolodexTestCase(TestCase):
             'ascii_short': person.ascii_short,
             'user': user.username,
             'email-0-person':person.pk,
-            'email-0-address': person.email_address(),
-            'email-TOTAL_FORMS':1,
+            'email-0-address': email.address,
+            'email-0-origin': email.origin,
+            'email-1-person':person.pk,
+            'email-1-address': 'name@example.com',
+            'email-1-origin': 'role: %s %s' % (group.acronym, role.name.slug),
+            'email-TOTAL_FORMS':2,
             'email-INITIAL_FORMS':1,
             'email-MIN_NUM_FORMS':0,
             'email-MAX_NUM_FORMS':1000,
diff --git a/ietf/secr/static/secr/css/custom.css b/ietf/secr/static/secr/css/custom.css
index 69353c0dd..d94427252 100644
--- a/ietf/secr/static/secr/css/custom.css
+++ b/ietf/secr/static/secr/css/custom.css
@@ -612,11 +612,11 @@ td.document-name {
    ========================================================================== */
 
 form[id^="rolodex-"] input[type=text] {
-    width: 30em;
+    width: 25em;
 }
 
 form[id^="rolodex-"] #id_address {
-    width: 30em;
+    width: 25em;
     height: 7em;
 }