Merged in ^/personal/henrik/6.79.1-gdpr@15180 containing GDPR-adaptations.

- Legacy-Id: 15181
This commit is contained in:
Henrik Levkowetz 2018-05-28 11:26:24 +00:00
commit 4db4dd16b0
46 changed files with 534 additions and 136 deletions

View file

@ -1,3 +1,67 @@
ietfdb (6.81.0) ietf; urgency=medium
This release contains code and schema changes necessary for compliance
with the European GDPR (General Data Protection Regulation). It provides
necessary GUI and internal changes, but lacks some admin utilities, and
also data migration routines which takes some time to run, and therefore
will be packaged in a separate release and applied separately.
From the commit log:
* Updated the personal information page with reviewed text from legal
counsel. Fixes issue #2503.
* Removed the Person.address field, which is not being used. This was a
legacy from the 2001 perl-based datatracker tables. Fixes issue #2504.
* 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.
* 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.
* Added origin information to all places where we create email address
entries.
* Removed all references to the removed Person.affiliation field.
* Added email origin information to some function calls that needed it.
* Overwrite earlier email origin when we've picked up the address from a
submission.
* Added a consent field to the Person model.
* Disallow profile changes without consent given. Together with previous
commits this fixes issues #2505 and #2507.
* Added another category of personal information to the
personal-information page, after review of personal information in the
code. Completes issue #2501.
* 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.
* Simplified the email.origin assignment code for outgoing liaisons.
* Fixed a long-standing bug in the liaison.name() code.
* Added assingment of the person.name_from_draft field on draft
submission.
* Updated the edit_profile template with information about consent-based
fields. Fixes issue #2502.
-- Henrik Levkowetz <henrik@levkowetz.com> 28 May 2018 11:15:34 +0000
ietfdb (6.80.1) ietf; urgency=medium
This is a bugfix release which also clears the slate for the upcoming

View file

@ -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=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(address="basea_author")
e = Email.objects.create(address="basea_author@example.com", person=p)
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)
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)
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)
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"))

View file

@ -954,6 +954,9 @@ def edit_shepherd(request, name):
events = []
doc.shepherd = form.cleaned_data['shepherd']
if doc.shepherd and 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)")

View file

@ -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":

View file

@ -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:

View file

@ -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')

View file

@ -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'),
]

View file

@ -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
@ -94,6 +98,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)
@ -105,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():
@ -135,6 +148,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)

View file

@ -207,9 +207,9 @@ 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,
"consent": True,
}
# edit details - faulty ASCII
@ -309,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)
Email.objects.create(address=user.username, person=p, origin=user.username)
# get
r = self.client.get(url)
@ -419,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)
Email.objects.create(address=user.username, person=p, origin=user.username)
# log in
r = self.client.post(redir_url, {"username":user.username, "password":"password"})
@ -466,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)
Email.objects.create(address="othername@example.org", person=p)
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"})

View file

@ -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
@ -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.
@ -293,7 +295,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,

View file

@ -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,6 +504,7 @@ class OutgoingLiaisonForm(LiaisonModelForm):
if has_role(self.user, "Liaison Manager"):
self.fields['to_groups'].initial = [queryset.first()]
class EditLiaisonForm(LiaisonModelForm):
def __init__(self, *args, **kwargs):
super(EditLiaisonForm, self).__init__(*args, **kwargs)

View file

@ -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

View file

@ -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='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)
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)
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,

View file

@ -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)
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

View file

@ -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()
@ -694,6 +694,7 @@ class NomcomViewsTest(TestCase):
# check objects
email = Email.objects.get(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],
@ -966,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
@ -1715,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)

View file

@ -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="nominee: %s" % nomcom.group.acronym)
person = Person.objects.create(name=candidate_name,
ascii=unidecode_name(candidate_name),
)

View file

@ -1,11 +1,11 @@
from django.contrib import admin
import simple_history
from ietf.person.models import Email, Alias, Person, PersonHistory, PersonalApiKey, PersonEvent, PersonApiKeyEvent
from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent
from ietf.person.name import name_parts
class EmailAdmin(admin.ModelAdmin):
list_display = ["address", "person", "time", "active", ]
class EmailAdmin(simple_history.admin.SimpleHistoryAdmin):
list_display = ["address", "person", "time", "active", "origin"]
raw_id_fields = ["person", ]
search_fields = ["address", "person__name", ]
admin.site.register(Email, EmailAdmin)
@ -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)
@ -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,5 @@ class PersonApiKeyEventAdmin(admin.ModelAdmin):
search_fields = ["person__name", ]
raw_id_fields = ['person', ]
admin.site.register(PersonApiKeyEvent, PersonApiKeyEventAdmin)

View file

@ -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)
@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 = factory.LazyAttribute(lambda obj: obj.person.user.username if obj.person.user else '')

View file

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# 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', '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=[
('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)')),
('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)),
('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.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'),
),
migrations.AddField(
model_name='historicalperson',
name='consent',
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.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'),
),
]

View file

@ -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
@ -24,10 +25,13 @@ 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
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.
@ -36,11 +40,11 @@ class PersonInfo(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.")
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)
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.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()
@ -144,24 +148,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)
@ -196,13 +197,8 @@ class Person(PersonInfo):
ct1['href'] = urljoin(hostscheme, self.json_url())
ct1['name'] = self.name
ct1['ascii'] = self.ascii
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
@ -231,12 +227,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
@ -280,6 +279,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"

View file

@ -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, HistoricalEmail)
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,57 @@ 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())
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())

View file

@ -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)
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):

View file

@ -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', 'biography', 'photo', 'photo_thumb', 'name_from_draft'):
if getattr(source,field) and not getattr(target,field):
setattr(target,field,getattr(source,field))
target.save()

View file

@ -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="import: %s" % __name__)
if created:
print "created email", email

View file

@ -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:

View file

@ -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 email and not email.origin:
email.origin = 'shepherd: %s' % m.name
email.save()
# handle replaced by
return m

View file

@ -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')

View file

@ -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:

View file

@ -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)

View file

@ -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',
}
@ -62,8 +61,6 @@ class RolodexTestCase(TestCase):
'name': person.name,
'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(),

View file

@ -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:

View file

@ -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>

View file

@ -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 %}

View file

@ -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

View file

@ -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='registration: ietf-%s'%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

View file

@ -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

View file

@ -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)
@ -437,9 +437,12 @@ def ensure_person_email_info_exists(name, email):
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)):
@ -452,6 +455,8 @@ def ensure_person_email_info_exists(name, email):
try:
email = person.email_set.get(address=addr)
email.origin = "author: %s" % docname # overwrite earlier origin
email.save()
except Email.DoesNotExist:
try:
# An Email object pointing to some other person will not exist
@ -463,10 +468,10 @@ def ensure_person_email_info_exists(name, email):
# most likely we just need to create it
email = Email(address=addr)
email.active = active
email.person = person
if email.time is None:
email.time = datetime.datetime.now()
email.origin = "author: %s" % docname
email.save()
return person, email
@ -474,7 +479,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:

View file

@ -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 %}

View file

@ -0,0 +1,96 @@
{% 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>
<!-- *** 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 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 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">
EU's General Data Protection Regulation </a>.
</p>
<p>
The datatracker treats all personal information derived from draft documents and
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.
</p>
<p>
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 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>
<li>Personal notification subscriptions</li>
</ul>
<p>
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 to the extent feasible and legally
permitted.
</p>
</div>
{% endblock %}

View file

@ -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>
</div>

View file

@ -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">

View file

@ -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:

View file

@ -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).

View file

@ -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=user.username)
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=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)
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)
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)
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)
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

View file

@ -367,8 +367,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 ...

View file

@ -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