Merged in ^/personal/henrik/6.79.1-gdpr@15180 containing GDPR-adaptations.
- Legacy-Id: 15181
This commit is contained in:
commit
4db4dd16b0
64
changelog
64
changelog
|
@ -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
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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 '')
|
||||
|
|
110
ietf/person/migrations/0003_auto_20180504_1519.py
Normal file
110
ietf/person/migrations/0003_auto_20180504_1519.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 %}
|
||||
|
|
96
ietf/templates/help/personal-information.html
Normal file
96
ietf/templates/help/personal-information.html
Normal 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) 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 %}
|
|
@ -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>
|
||||
|
|
|
@ -12,22 +12,57 @@
|
|||
{% origin %}
|
||||
<h1>Profile for {{ user.username }}</h1>
|
||||
|
||||
<p>
|
||||
Personal information in the datatracker which is derived from your contributions
|
||||
to the IETF standards development process is covered by the EU General Data Protection
|
||||
Regulation's
|
||||
<a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1">Article 6(1) (f)</a>
|
||||
covering IETF's Legitimate Interest due to the IETF's mission of developing standards
|
||||
for the internet. See also the page on <a href="/help/personal-information">handling
|
||||
of personal information</a>.
|
||||
|
||||
</p>
|
||||
<p>
|
||||
|
||||
Personal information which is <b>not</b> derived from your contributions is covered by the EU
|
||||
<a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679#d1e1888-1-1">GDPR Article 6(1) (a)</a>
|
||||
regarding consent. All such information is visible on this page, and is shown with the
|
||||
dagger symbol † next to it. Most of this
|
||||
information can be edited or removed on this page. There are some exceptions, such
|
||||
as photos, which currently require an email to <a href="mailto:ietf-action@ietf.org">the Secretariat</a>
|
||||
if you wish to update or remove the information.
|
||||
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<form class="form-horizontal" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form_errors person_form 'non_fields' %}
|
||||
{% bootstrap_form_errors person_form %}
|
||||
{% for f in new_email_forms %}
|
||||
{% bootstrap_form_errors f %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">User name</label>
|
||||
<label class="col-sm-2 control-label">User name †</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static">{{ user.username }}</p>
|
||||
<p class="form-control-static">
|
||||
{{ user.username }}
|
||||
<a href="/accounts/username/"><span class="fa fa-pencil"></span></a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Password</label>
|
||||
<label class="col-sm-2 control-label">Password †</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static"><a href="{% url 'ietf.ietfauth.views.change_password' %}">Password change form</a></p>
|
||||
<p class="form-control-static">
|
||||
|
||||
<a href="/accounts/password/">Password change form</a>
|
||||
<a href="/accounts/password/"><span class="fa fa-pencil"></span></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -78,7 +113,12 @@
|
|||
{% bootstrap_field role.email_form.email layout="horizontal" show_label=False %}
|
||||
{% endfor %}
|
||||
|
||||
{% bootstrap_form person_form layout="horizontal" %}
|
||||
{% bootstrap_field person_form.name layout="horizontal" %}
|
||||
{% bootstrap_field person_form.ascii layout="horizontal" %}
|
||||
{% if roles %}
|
||||
{% bootstrap_field person_form.biography layout="horizontal" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field person_form.consent layout="horizontal" %}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ...
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue