Improved SearchablePersonField to show the primary email address for any search results where a name appears more than once.
Simplified the edit nominee form. Replaced the merge nominee form with a request to the secretariat to merge Person records. Fixes #1847. Added merging nominees to the secretariat's person merging script. Restructured the person merging script to make it testable. Updated some tests to match changes to the mailtriggers that hadn't made it to the fixtures. - Legacy-Id: 10625
This commit is contained in:
parent
aadcf2d056
commit
dc5ae398f2
|
@ -12,11 +12,10 @@ import django
|
|||
django.setup()
|
||||
|
||||
import argparse
|
||||
import pprint
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
from ietf.person.models import Person
|
||||
|
||||
from ietf.person.utils import merge_persons
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source_id",type=int)
|
||||
parser.add_argument("target_id",type=int)
|
||||
|
@ -30,62 +29,4 @@ response = raw_input('Ok to continue y/n? ')
|
|||
if response.lower() != 'y':
|
||||
sys.exit()
|
||||
|
||||
# merge emails
|
||||
for email in source.email_set.all():
|
||||
print "Merging email: {}".format(email.address)
|
||||
email.person = target
|
||||
email.save()
|
||||
|
||||
# merge aliases
|
||||
target_aliases = [ a.name for a in target.alias_set.all() ]
|
||||
for alias in source.alias_set.all():
|
||||
if alias.name in target_aliases:
|
||||
alias.delete()
|
||||
else:
|
||||
print "Merging alias: {}".format(alias.name)
|
||||
alias.person = target
|
||||
alias.save()
|
||||
|
||||
# merge DocEvents
|
||||
for docevent in source.docevent_set.all():
|
||||
docevent.by = target
|
||||
docevent.save()
|
||||
|
||||
# merge SubmissionEvents
|
||||
for subevent in source.submissionevent_set.all():
|
||||
subevent.by = target
|
||||
subevent.save()
|
||||
|
||||
# merge Messages
|
||||
for message in source.message_set.all():
|
||||
message.by = target
|
||||
message.save()
|
||||
|
||||
# merge Constraints
|
||||
for constraint in source.constraint_set.all():
|
||||
constraint.person = target
|
||||
constraint.save()
|
||||
|
||||
# merge Roles
|
||||
for role in source.role_set.all():
|
||||
role.person = target
|
||||
role.save()
|
||||
|
||||
# check for any remaining relationships and delete if none
|
||||
objs = [source]
|
||||
opts = Person._meta
|
||||
user = User.objects.filter(is_superuser=True).first()
|
||||
admin_site = admin.site
|
||||
using = 'default'
|
||||
|
||||
deletable_objects, perms_needed, protected = admin.utils.get_deleted_objects(
|
||||
objs, opts, user, admin_site, using)
|
||||
|
||||
if len(deletable_objects) > 1:
|
||||
print "Not Deleting Person: {}({})".format(source.ascii,source.pk)
|
||||
print "Related objects remain:"
|
||||
pprint.pprint(deletable_objects[1])
|
||||
|
||||
else:
|
||||
print "Deleting Person: {}({})".format(source.ascii,source.pk)
|
||||
source.delete()
|
||||
merge_persons(source,target,sys.stdout)
|
||||
|
|
|
@ -1268,6 +1268,7 @@ class ChangeReplacesTests(TestCase):
|
|||
expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
|
||||
group=mars_wg,
|
||||
)
|
||||
self.basea.documentauthor_set.create(author=Email.objects.create(address="basea_author@example.com"),order=1)
|
||||
|
||||
self.baseb = Document.objects.create(
|
||||
name="draft-test-base-b",
|
||||
|
@ -1278,6 +1279,7 @@ class ChangeReplacesTests(TestCase):
|
|||
expires=datetime.datetime.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
|
||||
group=mars_wg,
|
||||
)
|
||||
self.baseb.documentauthor_set.create(author=Email.objects.create(address="baseb_author@example.com"),order=1)
|
||||
|
||||
self.replacea = Document.objects.create(
|
||||
name="draft-test-replace-a",
|
||||
|
@ -1288,6 +1290,7 @@ class ChangeReplacesTests(TestCase):
|
|||
expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
|
||||
group=mars_wg,
|
||||
)
|
||||
self.replacea.documentauthor_set.create(author=Email.objects.create(address="replacea_author@example.com"),order=1)
|
||||
|
||||
self.replaceboth = Document.objects.create(
|
||||
name="draft-test-replace-both",
|
||||
|
@ -1298,6 +1301,7 @@ class ChangeReplacesTests(TestCase):
|
|||
expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
|
||||
group=mars_wg,
|
||||
)
|
||||
self.replaceboth.documentauthor_set.create(author=Email.objects.create(address="replaceboth_author@example.com"),order=1)
|
||||
|
||||
self.basea.set_state(State.objects.get(used=True, type="draft", slug="active"))
|
||||
self.baseb.set_state(State.objects.get(used=True, type="draft", slug="expired"))
|
||||
|
@ -1332,8 +1336,8 @@ class ChangeReplacesTests(TestCase):
|
|||
self.assertTrue(not RelatedDocument.objects.filter(relationship='possibly-replaces', source=self.replacea))
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue('replacement status updated' in outbox[-1]['Subject'])
|
||||
self.assertTrue('base-a@' in outbox[-1]['To'])
|
||||
self.assertTrue('replace-a@' in outbox[-1]['To'])
|
||||
self.assertTrue('replacea_author@' in outbox[-1]['To'])
|
||||
self.assertTrue('basea_author@' in outbox[-1]['To'])
|
||||
|
||||
empty_outbox()
|
||||
# Post that says replaceboth replaces both base a and base b
|
||||
|
@ -1344,9 +1348,9 @@ class ChangeReplacesTests(TestCase):
|
|||
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
|
||||
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl')
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue('base-a@' in outbox[-1]['To'])
|
||||
self.assertTrue('base-b@' in outbox[-1]['To'])
|
||||
self.assertTrue('replace-both@' in outbox[-1]['To'])
|
||||
self.assertTrue('basea_author@' in outbox[-1]['To'])
|
||||
self.assertTrue('baseb_author@' in outbox[-1]['To'])
|
||||
self.assertTrue('replaceboth_author@' in outbox[-1]['To'])
|
||||
|
||||
# Post that undoes replaceboth
|
||||
empty_outbox()
|
||||
|
@ -1355,9 +1359,9 @@ class ChangeReplacesTests(TestCase):
|
|||
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea
|
||||
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired')
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue('base-a@' in outbox[-1]['To'])
|
||||
self.assertTrue('base-b@' in outbox[-1]['To'])
|
||||
self.assertTrue('replace-both@' in outbox[-1]['To'])
|
||||
self.assertTrue('basea_author@' in outbox[-1]['To'])
|
||||
self.assertTrue('baseb_author@' in outbox[-1]['To'])
|
||||
self.assertTrue('replaceboth_author@' in outbox[-1]['To'])
|
||||
|
||||
# Post that undoes replacea
|
||||
empty_outbox()
|
||||
|
@ -1365,8 +1369,8 @@ class ChangeReplacesTests(TestCase):
|
|||
r = self.client.post(url, dict(replaces=""))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')
|
||||
self.assertTrue('base-a@' in outbox[-1]['To'])
|
||||
self.assertTrue('replace-a@' in outbox[-1]['To'])
|
||||
self.assertTrue('basea_author@' in outbox[-1]['To'])
|
||||
self.assertTrue('replacea_author@' in outbox[-1]['To'])
|
||||
|
||||
|
||||
def test_review_possibly_replaces(self):
|
||||
|
|
|
@ -20,6 +20,8 @@ from ietf.utils import draft, markup_txt
|
|||
from ietf.utils.mail import send_mail
|
||||
from ietf.mailtrigger.utils import gather_address_lists
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
#TODO FIXME - it would be better if this lived in ietf/doc/mails.py, but there's
|
||||
# an import order issue to work out.
|
||||
def email_update_telechat(request, doc, text):
|
||||
|
|
28
ietf/mailtrigger/migrations/0003_merge_request_trigger.py
Normal file
28
ietf/mailtrigger/migrations/0003_merge_request_trigger.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
|
||||
Recipient=apps.get_model('mailtrigger','Recipient')
|
||||
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
|
||||
|
||||
m = MailTrigger.objects.create(
|
||||
slug='person_merge_requested',
|
||||
desc="Recipients for a message requesting that duplicated Person records be merged ")
|
||||
m.to = Recipient.objects.filter(slug__in=['ietf_secretariat', ])
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
|
||||
MailTrigger.objects.filter(slug='person_merge_requested').delete()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailtrigger', '0002_auto_20150809_1314'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse)
|
||||
]
|
|
@ -648,6 +648,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "charter",
|
||||
"used": true,
|
||||
"name": "Charter",
|
||||
"desc": ""
|
||||
|
@ -658,6 +659,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "agenda",
|
||||
"used": true,
|
||||
"name": "Agenda",
|
||||
"desc": ""
|
||||
|
@ -668,6 +670,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "minutes",
|
||||
"used": true,
|
||||
"name": "Minutes",
|
||||
"desc": ""
|
||||
|
@ -678,6 +681,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "slides",
|
||||
"used": true,
|
||||
"name": "Slides",
|
||||
"desc": ""
|
||||
|
@ -688,6 +692,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "draft",
|
||||
"used": true,
|
||||
"name": "Draft",
|
||||
"desc": ""
|
||||
|
@ -698,6 +703,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "liai-att",
|
||||
"used": true,
|
||||
"name": "Liaison Attachment",
|
||||
"desc": ""
|
||||
|
@ -708,6 +714,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "conflict-review",
|
||||
"used": true,
|
||||
"name": "Conflict Review",
|
||||
"desc": ""
|
||||
|
@ -718,6 +725,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "status-change",
|
||||
"used": true,
|
||||
"name": "Status Change",
|
||||
"desc": ""
|
||||
|
@ -728,6 +736,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "",
|
||||
"used": false,
|
||||
"name": "Shepherd's writeup",
|
||||
"desc": ""
|
||||
|
@ -738,6 +747,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "",
|
||||
"used": false,
|
||||
"name": "Liaison",
|
||||
"desc": ""
|
||||
|
@ -748,6 +758,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "recording",
|
||||
"used": true,
|
||||
"name": "Recording",
|
||||
"desc": ""
|
||||
|
@ -758,6 +769,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"order": 0,
|
||||
"prefix": "bluesheets",
|
||||
"used": true,
|
||||
"name": "Bluesheets",
|
||||
"desc": ""
|
||||
|
@ -1580,6 +1592,16 @@
|
|||
"model": "name.liaisonstatementeventtypename",
|
||||
"pk": "comment"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"order": 10,
|
||||
"used": true,
|
||||
"name": "Private Comment",
|
||||
"desc": ""
|
||||
},
|
||||
"model": "name.liaisonstatementeventtypename",
|
||||
"pk": "private_comment"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"order": 1,
|
||||
|
@ -4541,6 +4563,14 @@
|
|||
"model": "mailtrigger.recipient",
|
||||
"pk": "doc_authors"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"template": "{{doc.author_list}}",
|
||||
"desc": "The authors of the document, without using the draft aliases"
|
||||
},
|
||||
"model": "mailtrigger.recipient",
|
||||
"pk": "doc_authors_expanded"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"template": null,
|
||||
|
@ -4701,6 +4731,14 @@
|
|||
"model": "mailtrigger.recipient",
|
||||
"pk": "iana_last_call"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"template": "<i-d-announce@ietf.org>",
|
||||
"desc": "The I-D-Announce Email List"
|
||||
},
|
||||
"model": "mailtrigger.recipient",
|
||||
"pk": "id_announce"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"template": "The IESG <iesg@ietf.org>",
|
||||
|
@ -5306,11 +5344,7 @@
|
|||
"fields": {
|
||||
"cc": [],
|
||||
"to": [
|
||||
"doc_authors",
|
||||
"doc_group_chairs",
|
||||
"doc_group_responsible_directors",
|
||||
"doc_notify",
|
||||
"doc_shepherd"
|
||||
"doc_authors_expanded"
|
||||
],
|
||||
"desc": "Recipients when what a document replaces or is replaced by changes"
|
||||
},
|
||||
|
@ -5698,6 +5732,17 @@
|
|||
"model": "mailtrigger.mailtrigger",
|
||||
"pk": "nomination_received"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"cc": [],
|
||||
"to": [
|
||||
"ietf_secretariat"
|
||||
],
|
||||
"desc": "Recipients for a message requesting that duplicated Person records be merged "
|
||||
},
|
||||
"model": "mailtrigger.mailtrigger",
|
||||
"pk": "person_merge_requested"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"cc": [
|
||||
|
@ -5843,7 +5888,7 @@
|
|||
"submission_group_mail_list"
|
||||
],
|
||||
"to": [
|
||||
"ietf_announce"
|
||||
"id_announce"
|
||||
],
|
||||
"desc": "Recipients for the announcement of a successfully submitted draft"
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.html import mark_safe
|
|||
from ietf.dbtemplate.forms import DBTemplateForm
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.name.models import RoleName, FeedbackTypeName, NomineePositionStateName
|
||||
from ietf.name.models import RoleName, FeedbackTypeName
|
||||
from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition,
|
||||
Position, Feedback, ReminderDates )
|
||||
from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE,
|
||||
|
@ -19,7 +19,7 @@ from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEM
|
|||
make_nomineeposition, make_nomineeposition_for_newperson,
|
||||
create_feedback_email)
|
||||
from ietf.person.models import Email
|
||||
from ietf.person.fields import SearchableEmailField
|
||||
from ietf.person.fields import SearchableEmailField, SearchablePersonField, SearchablePersonsField
|
||||
from ietf.utils.fields import MultiEmailField
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.mailtrigger.utils import gather_address_lists
|
||||
|
@ -223,85 +223,31 @@ class EditNomcomForm(forms.ModelForm):
|
|||
|
||||
class MergeForm(forms.Form):
|
||||
|
||||
secondary_emails = MultiEmailField(label="Secondary email addresses",
|
||||
help_text="Provide a comma separated list of email addresses. Nominations already received with any of these email address will be moved to show under the primary address.", widget=forms.Textarea)
|
||||
primary_email = forms.EmailField(label="Primary email address",
|
||||
widget=forms.TextInput(attrs={'size': '40'}))
|
||||
primary_person = SearchablePersonField(help_text="Select the person you want the datatracker to keep")
|
||||
duplicate_persons = SearchablePersonsField(help_text="Select all the duplicates that should be merged into the primary person record")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.nomcom = kwargs.pop('nomcom', None)
|
||||
super(MergeForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_primary_email(self):
|
||||
email = self.cleaned_data['primary_email']
|
||||
nominees = Nominee.objects.get_by_nomcom(self.nomcom).not_duplicated().filter(email__address=email)
|
||||
if not nominees:
|
||||
msg = "No nominee with this email exists"
|
||||
self._errors["primary_email"] = self.error_class([msg])
|
||||
|
||||
return email
|
||||
|
||||
def clean_secondary_emails(self):
|
||||
emails = self.cleaned_data['secondary_emails']
|
||||
for email in emails:
|
||||
nominees = Nominee.objects.get_by_nomcom(self.nomcom).not_duplicated().filter(email__address=email)
|
||||
if not nominees:
|
||||
msg = "No nominee with email %s exists" % email
|
||||
self._errors["primary_email"] = self.error_class([msg])
|
||||
break
|
||||
|
||||
return emails
|
||||
|
||||
def clean(self):
|
||||
primary_email = self.cleaned_data.get("primary_email")
|
||||
secondary_emails = self.cleaned_data.get("secondary_emails")
|
||||
if primary_email and secondary_emails:
|
||||
if primary_email in secondary_emails:
|
||||
msg = "Primary and secondary email address must be differents"
|
||||
self._errors["primary_email"] = self.error_class([msg])
|
||||
primary_person = self.cleaned_data.get("primary_person")
|
||||
duplicate_persons = self.cleaned_data.get("duplicate_persons")
|
||||
if primary_person and duplicate_persons:
|
||||
if primary_person in duplicate_persons:
|
||||
msg = "The primary person must not also be listed as a duplicate person"
|
||||
self._errors["primary_person"] = self.error_class([msg])
|
||||
return self.cleaned_data
|
||||
|
||||
def save(self):
|
||||
primary_email = self.cleaned_data.get("primary_email")
|
||||
secondary_emails = self.cleaned_data.get("secondary_emails")
|
||||
|
||||
primary_nominee = Nominee.objects.get_by_nomcom(self.nomcom).get(email__address=primary_email)
|
||||
while primary_nominee.duplicated:
|
||||
primary_nominee = primary_nominee.duplicated
|
||||
secondary_nominees = Nominee.objects.get_by_nomcom(self.nomcom).filter(email__address__in=secondary_emails)
|
||||
for nominee in secondary_nominees:
|
||||
# move nominations
|
||||
nominee.nomination_set.all().update(nominee=primary_nominee)
|
||||
# move feedback
|
||||
for fb in nominee.feedback_set.all():
|
||||
fb.nominees.remove(nominee)
|
||||
fb.nominees.add(primary_nominee)
|
||||
# move nomineepositions
|
||||
for nominee_position in nominee.nomineeposition_set.all():
|
||||
primary_nominee_positions = NomineePosition.objects.filter(position=nominee_position.position,
|
||||
nominee=primary_nominee)
|
||||
primary_nominee_position = primary_nominee_positions and primary_nominee_positions[0] or None
|
||||
|
||||
if primary_nominee_position:
|
||||
# if already a nomineeposition object for a position and nominee,
|
||||
# update the nomineepostion of primary nominee with the state
|
||||
if nominee_position.state.slug == 'accepted' or primary_nominee_position.state.slug == 'accepted':
|
||||
primary_nominee_position.state = NomineePositionStateName.objects.get(slug='accepted')
|
||||
primary_nominee_position.save()
|
||||
if nominee_position.state.slug == 'declined' and primary_nominee_position.state.slug == 'pending':
|
||||
primary_nominee_position.state = NomineePositionStateName.objects.get(slug='declined')
|
||||
primary_nominee_position.save()
|
||||
else:
|
||||
# It is not allowed two or more nomineeposition objects with same position and nominee
|
||||
# move nominee_position object to primary nominee
|
||||
nominee_position.nominee = primary_nominee
|
||||
nominee_position.save()
|
||||
|
||||
nominee.duplicated = primary_nominee
|
||||
nominee.save()
|
||||
|
||||
secondary_nominees.update(duplicated=primary_nominee)
|
||||
primary_person = self.cleaned_data.get("primary_person")
|
||||
duplicate_persons = self.cleaned_data.get("duplicate_persons")
|
||||
|
||||
subject = "Request to merge Person records"
|
||||
from_email = settings.NOMCOM_FROM_EMAIL
|
||||
(to_email, cc) = gather_address_lists('person_merge_requested')
|
||||
context = {'primary_person':primary_person, 'duplicate_persons':duplicate_persons}
|
||||
send_mail(None, to_email, from_email, subject, 'nomcom/merge_request.txt', context, cc=cc)
|
||||
|
||||
class NominateForm(forms.ModelForm):
|
||||
searched_email = SearchableEmailField(only_users=False)
|
||||
|
@ -813,45 +759,25 @@ FullFeedbackFormSet = forms.modelformset_factory(
|
|||
|
||||
class EditNomineeForm(forms.ModelForm):
|
||||
|
||||
nominee_email = forms.EmailField(label="Nominee email",
|
||||
widget=forms.TextInput(attrs={'size': '40'}))
|
||||
nominee_email = forms.ModelChoiceField(queryset=Email.objects.none(),empty_label=None)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditNomineeForm, self).__init__(*args, **kwargs)
|
||||
self.fields['nominee_email'].initial = self.instance.email.address
|
||||
self.fields['nominee_email'].queryset = Email.objects.filter(person=self.instance.person,active=True)
|
||||
self.fields['nominee_email'].initial = self.instance.email
|
||||
self.fields['nominee_email'].help_text = "If the address you are looking for does not appear in this list, ask the nominee (or the secretariat) to add the address to thier datatracker account and ensure it is marked as active."
|
||||
|
||||
def save(self, commit=True):
|
||||
nominee = super(EditNomineeForm, self).save(commit=False)
|
||||
nominee_email = self.cleaned_data.get("nominee_email")
|
||||
if nominee_email != nominee.email.address:
|
||||
# create a new nominee with the new email
|
||||
new_email, created_email = Email.objects.get_or_create(address=nominee_email)
|
||||
new_email.person = nominee.email.person
|
||||
new_email.save()
|
||||
|
||||
# Chage emails between nominees
|
||||
old_email = nominee.email
|
||||
nominee.email = new_email
|
||||
nominee.save()
|
||||
new_nominee = Nominee.objects.create(email=old_email, nomcom=nominee.nomcom)
|
||||
|
||||
# new nominees point to old nominee
|
||||
new_nominee.duplicated = nominee
|
||||
new_nominee.save()
|
||||
|
||||
nominee.email = nominee_email
|
||||
nominee.save()
|
||||
return nominee
|
||||
|
||||
class Meta:
|
||||
model = Nominee
|
||||
fields = ('nominee_email',)
|
||||
|
||||
def clean_nominee_email(self):
|
||||
nominee_email = self.cleaned_data['nominee_email']
|
||||
nominees = Nominee.objects.exclude(email__address=self.instance.email.address).filter(email__address=nominee_email)
|
||||
if nominees:
|
||||
raise forms.ValidationError('This emails already does exists in another nominee, please go to merge form')
|
||||
return nominee_email
|
||||
|
||||
class NominationResponseCommentForm(forms.Form):
|
||||
comments = forms.CharField(widget=forms.Textarea,required=False,help_text="Any comments provided will be encrytped and will only be visible to the NomCom.")
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import datetime
|
|||
import os
|
||||
import shutil
|
||||
from pyquery import PyQuery
|
||||
import StringIO
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Max
|
||||
|
@ -21,6 +22,8 @@ from ietf.person.models import Email, Person
|
|||
from ietf.group.models import Group
|
||||
from ietf.message.models import Message
|
||||
|
||||
from ietf.person.utils import merge_persons
|
||||
|
||||
from ietf.nomcom.test_data import nomcom_test_data, generate_cert, check_comments, \
|
||||
COMMUNITY_USER, CHAIR_USER, \
|
||||
MEMBER_USER, SECRETARIAT_USER, EMAIL_DOMAIN, NOMCOM_YEAR
|
||||
|
@ -34,7 +37,7 @@ from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_s
|
|||
from ietf.nomcom.factories import NomComFactory, FeedbackFactory, \
|
||||
nomcom_kwargs_for_year, provide_private_key_to_test_client, \
|
||||
key
|
||||
from ietf.person.factories import PersonFactory, EmailFactory
|
||||
from ietf.person.factories import PersonFactory, EmailFactory, UserFactory
|
||||
from ietf.dbtemplate.factories import DBTemplateFactory
|
||||
from ietf.dbtemplate.models import DBTemplate
|
||||
|
||||
|
@ -169,205 +172,6 @@ class NomcomViewsTest(TestCase):
|
|||
self.client.logout()
|
||||
|
||||
|
||||
def test_private_merge_view(self):
|
||||
"""Verify private merge view"""
|
||||
|
||||
nominees = [u'nominee0@example.com',
|
||||
u'nominee1@example.com',
|
||||
u'nominee2@example.com',
|
||||
u'nominee3@example.com']
|
||||
|
||||
# do nominations
|
||||
login_testing_unauthorized(self, COMMUNITY_USER, self.public_nominate_url)
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[0],
|
||||
position='IAOC')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[0],
|
||||
position='IAOC')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[0],
|
||||
position='IAB')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[0],
|
||||
position='TSV')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[1],
|
||||
position='IAOC')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[1],
|
||||
position='IAOC')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[2],
|
||||
position='IAB')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[2],
|
||||
position='IAB')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[3],
|
||||
position='TSV')
|
||||
self.nominate_view(public=True,
|
||||
nominee_email=nominees[3],
|
||||
position='TSV')
|
||||
# Check nominee positions
|
||||
self.assertEqual(NomineePosition.objects.count(), 6)
|
||||
self.assertEqual(Feedback.objects.nominations().count(), 10)
|
||||
|
||||
# Accept and declined nominations
|
||||
nominee_position = NomineePosition.objects.get(position__name='IAOC',
|
||||
nominee__email__address=nominees[0])
|
||||
nominee_position.state = NomineePositionStateName.objects.get(slug='accepted')
|
||||
nominee_position.save()
|
||||
|
||||
nominee_position = NomineePosition.objects.get(position__name='IAOC',
|
||||
nominee__email__address=nominees[1])
|
||||
nominee_position.state = NomineePositionStateName.objects.get(slug='declined')
|
||||
nominee_position.save()
|
||||
|
||||
nominee_position = NomineePosition.objects.get(position__name='IAB',
|
||||
nominee__email__address=nominees[2])
|
||||
nominee_position.state = NomineePositionStateName.objects.get(slug='declined')
|
||||
nominee_position.save()
|
||||
|
||||
nominee_position = NomineePosition.objects.get(position__name='TSV',
|
||||
nominee__email__address=nominees[3])
|
||||
nominee_position.state = NomineePositionStateName.objects.get(slug='accepted')
|
||||
nominee_position.save()
|
||||
|
||||
self.client.logout()
|
||||
|
||||
# fill questionnaires (internally the function does new nominations)
|
||||
self.access_chair_url(self.add_questionnaire_url)
|
||||
|
||||
self.add_questionnaire(public=False,
|
||||
nominee_email=nominees[0],
|
||||
position='IAOC')
|
||||
self.add_questionnaire(public=False,
|
||||
nominee_email=nominees[1],
|
||||
position='IAOC')
|
||||
self.add_questionnaire(public=False,
|
||||
nominee_email=nominees[2],
|
||||
position='IAB')
|
||||
self.add_questionnaire(public=False,
|
||||
nominee_email=nominees[3],
|
||||
position='TSV')
|
||||
self.assertEqual(Feedback.objects.questionnaires().count(), 4)
|
||||
|
||||
self.client.logout()
|
||||
|
||||
## Add feedbacks (internally the function does new nominations)
|
||||
self.access_member_url(self.private_feedback_url)
|
||||
self.feedback_view(public=False,
|
||||
nominee_email=nominees[0],
|
||||
position='IAOC')
|
||||
self.feedback_view(public=False,
|
||||
nominee_email=nominees[1],
|
||||
position='IAOC')
|
||||
self.feedback_view(public=False,
|
||||
nominee_email=nominees[2],
|
||||
position='IAB')
|
||||
self.feedback_view(public=False,
|
||||
nominee_email=nominees[3],
|
||||
position='TSV')
|
||||
|
||||
self.assertEqual(Feedback.objects.comments().count(), 4)
|
||||
self.assertEqual(Feedback.objects.nominations().count(), 18)
|
||||
self.assertEqual(Feedback.objects.nominations().filter(nominees__email__address=nominees[0]).count(), 6)
|
||||
self.assertEqual(Feedback.objects.nominations().filter(nominees__email__address=nominees[1]).count(), 4)
|
||||
self.assertEqual(Feedback.objects.nominations().filter(nominees__email__address=nominees[2]).count(), 4)
|
||||
self.assertEqual(Feedback.objects.nominations().filter(nominees__email__address=nominees[3]).count(), 4)
|
||||
for nominee in nominees:
|
||||
self.assertEqual(Feedback.objects.comments().filter(nominees__email__address=nominee).count(),
|
||||
1)
|
||||
self.assertEqual(Feedback.objects.questionnaires().filter(nominees__email__address=nominee).count(),
|
||||
1)
|
||||
|
||||
self.client.logout()
|
||||
|
||||
## merge nominations
|
||||
self.access_chair_url(self.private_merge_url)
|
||||
|
||||
test_data = {"secondary_emails": "%s, %s" % (nominees[0], nominees[1]),
|
||||
"primary_email": nominees[0]}
|
||||
response = self.client.post(self.private_merge_url, test_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue(q("form .has-error"))
|
||||
|
||||
test_data = {"primary_email": nominees[0],
|
||||
"secondary_emails": ""}
|
||||
response = self.client.post(self.private_merge_url, test_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue(q("form .has-error"))
|
||||
|
||||
test_data = {"primary_email": "",
|
||||
"secondary_emails": nominees[0]}
|
||||
response = self.client.post(self.private_merge_url, test_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue(q("form .has-error"))
|
||||
|
||||
test_data = {"primary_email": "unknown@example.com",
|
||||
"secondary_emails": nominees[0]}
|
||||
response = self.client.post(self.private_merge_url, test_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue(q("form .has-error"))
|
||||
|
||||
test_data = {"primary_email": nominees[0],
|
||||
"secondary_emails": "unknown@example.com"}
|
||||
response = self.client.post(self.private_merge_url, test_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue(q("form .has-error"))
|
||||
|
||||
test_data = {"secondary_emails": """%s,
|
||||
%s,
|
||||
%s""" % (nominees[1], nominees[2], nominees[3]),
|
||||
"primary_email": nominees[0]}
|
||||
|
||||
response = self.client.post(self.private_merge_url, test_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "alert-success")
|
||||
|
||||
self.assertEqual(Nominee.objects.filter(email__address=nominees[1],
|
||||
duplicated__isnull=False).count(), 1)
|
||||
self.assertEqual(Nominee.objects.filter(email__address=nominees[2],
|
||||
duplicated__isnull=False).count(), 1)
|
||||
self.assertEqual(Nominee.objects.filter(email__address=nominees[3],
|
||||
duplicated__isnull=False).count(), 1)
|
||||
|
||||
nominee = Nominee.objects.get(email__address=nominees[0])
|
||||
|
||||
self.assertEqual(Nomination.objects.filter(nominee=nominee).count(), 18)
|
||||
self.assertEqual(Feedback.objects.nominations().filter(nominees__in=[nominee]).count(),
|
||||
18)
|
||||
self.assertEqual(Feedback.objects.comments().filter(nominees__in=[nominee]).count(),
|
||||
4)
|
||||
self.assertEqual(Feedback.objects.questionnaires().filter(nominees__in=[nominee]).count(),
|
||||
4)
|
||||
|
||||
for nominee_email in nominees[1:]:
|
||||
self.assertEqual(Feedback.objects.nominations().filter(nominees__email__address=nominee_email).count(),
|
||||
0)
|
||||
self.assertEqual(Feedback.objects.comments().filter(nominees__email__address=nominee_email).count(),
|
||||
0)
|
||||
self.assertEqual(Feedback.objects.questionnaires().filter(nominees__email__address=nominee_email).count(),
|
||||
0)
|
||||
|
||||
self.assertEqual(NomineePosition.objects.filter(nominee=nominee).count(), 3)
|
||||
|
||||
# Check nominations state
|
||||
self.assertEqual(NomineePosition.objects.get(position__name='TSV',
|
||||
nominee=nominee).state.slug, u'accepted')
|
||||
self.assertEqual(NomineePosition.objects.get(position__name='IAOC',
|
||||
nominee=nominee).state.slug, u'accepted')
|
||||
self.assertEqual(NomineePosition.objects.get(position__name='IAB',
|
||||
nominee=nominee).state.slug, u'declined')
|
||||
|
||||
self.client.logout()
|
||||
|
||||
def change_members(self, members):
|
||||
members_emails = u','.join(['%s%s' % (member, EMAIL_DOMAIN) for member in members])
|
||||
test_data = {'members': members_emails,
|
||||
|
@ -1662,8 +1466,35 @@ Junk body for testing
|
|||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Note that the old tests currently test the edit_position view through its nomcom_add_position name
|
||||
|
||||
def test_edit_nominee(self):
|
||||
nominee = self.nc.nominee_set.first()
|
||||
new_email = EmailFactory(person=nominee.person)
|
||||
url = reverse('nomcom_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)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post(url,{'nominee_email':new_email.address})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
nominee = self.nc.nominee_set.first()
|
||||
self.assertEqual(nominee.email,new_email)
|
||||
|
||||
def test_request_merge(self):
|
||||
nominee1, nominee2 = self.nc.nominee_set.all()[:2]
|
||||
url = reverse('nomcom_private_merge',kwargs={'year':self.nc.year()})
|
||||
login_testing_unauthorized(self,self.chair.user.username,url)
|
||||
empty_outbox()
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post(url,{'primary_person':nominee1.person.pk,
|
||||
'duplicate_persons':[nominee1.person.pk]})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue('must not also be listed as a duplicate' in unicontent(response))
|
||||
response = self.client.post(url,{'primary_person':nominee1.person.pk,
|
||||
'duplicate_persons':[nominee2.person.pk]})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(outbox),1)
|
||||
self.assertTrue(all([str(x.person.pk) in unicode(outbox[0]) for x in [nominee1,nominee2]]))
|
||||
|
||||
|
||||
class NomComIndexTests(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -1705,3 +1536,32 @@ class NoPublicKeyTests(TestCase):
|
|||
# Warn on edit nomcom
|
||||
self.do_common_work(reverse('nomcom_edit_nomcom',kwargs={'year':self.nc.year()}),True)
|
||||
|
||||
class MergePersonTests(TestCase):
|
||||
def setUp(self):
|
||||
build_test_public_keys_dir(self)
|
||||
self.nc = NomComFactory(**nomcom_kwargs_for_year())
|
||||
self.author = PersonFactory.create().email_set.first().address
|
||||
self.nominee1, self.nominee2 = self.nc.nominee_set.all()[:2]
|
||||
self.person1, self.person2 = self.nominee1.person, self.nominee2.person
|
||||
self.position = self.nc.position_set.first()
|
||||
for nominee in [self.nominee1, self.nominee2]:
|
||||
f = FeedbackFactory.create(author=self.author,nomcom=self.nc,type_id='nomina')
|
||||
f.positions.add(self.position)
|
||||
f.nominees.add(nominee)
|
||||
UserFactory(is_superuser=True)
|
||||
|
||||
def tearDown(self):
|
||||
clean_test_public_keys_dir(self)
|
||||
|
||||
def test_merge_person(self):
|
||||
person1, person2 = [nominee.person for nominee in self.nc.nominee_set.all()[:2]]
|
||||
stream = StringIO.StringIO()
|
||||
|
||||
self.assertEqual(self.nc.nominee_set.count(),4)
|
||||
self.assertEqual(self.nominee1.feedback_set.count(),1)
|
||||
self.assertEqual(self.nominee2.feedback_set.count(),1)
|
||||
merge_persons(person1,person2,stream)
|
||||
self.assertEqual(self.nc.nominee_set.count(),3)
|
||||
self.assertEqual(self.nc.nominee_set.get(pk=self.nominee2.pk).feedback_set.count(),2)
|
||||
self.assertFalse(self.nc.nominee_set.filter(pk=self.nominee1.pk).exists())
|
||||
|
||||
|
|
|
@ -271,10 +271,11 @@ def private_merge(request, year):
|
|||
form = None
|
||||
else:
|
||||
if request.method == 'POST':
|
||||
form = MergeForm(request.POST, nomcom=nomcom)
|
||||
form = MergeForm(request.POST, nomcom=nomcom )
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'The emails have been unified')
|
||||
messages.success(request, 'A merge request has been sent to the secretariat.')
|
||||
return redirect('nomcom_private_index',year=year)
|
||||
else:
|
||||
form = MergeForm(nomcom=nomcom)
|
||||
|
||||
|
@ -784,7 +785,8 @@ def edit_nominee(request, year, nominee_id):
|
|||
instance=nominee)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'The nominee has been changed')
|
||||
messages.success(request, 'The nomination address for %s has been changed to %s'%(nominee.name(),nominee.email.address))
|
||||
return redirect('nomcom_private_index', year=year)
|
||||
else:
|
||||
form = EditNomineeForm(instance=nominee)
|
||||
|
||||
|
|
|
@ -53,6 +53,6 @@ class EmailFactory(factory.DjangoModelFactory):
|
|||
model = Email
|
||||
django_get_or_create = ('address',)
|
||||
|
||||
address = '%s.%s@%s' % (factory.Faker('first_name'),factory.Faker('last_name'),factory.Faker('domain_name'))
|
||||
address = '%s.%s@%s' % (fake.first_name(),fake.last_name(),fake.domain_name())
|
||||
active = True
|
||||
primary = False
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import json
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from django.utils.html import escape
|
||||
from django import forms
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
@ -12,7 +14,19 @@ def select2_id_name_json(objs):
|
|||
def format_email(e):
|
||||
return escape(u"%s <%s>" % (e.person.name, e.address))
|
||||
def format_person(p):
|
||||
return escape(p.name)
|
||||
if p.name_count > 1:
|
||||
return escape('%s (%s)' % (p.name,p.email().address))
|
||||
else:
|
||||
return escape(p.name)
|
||||
|
||||
if objs and isinstance(objs[0], Email):
|
||||
formatter = format_email
|
||||
else:
|
||||
formatter = format_person
|
||||
c = Counter([p.name for p in objs])
|
||||
for p in objs:
|
||||
p.name_count = c[p.name]
|
||||
|
||||
|
||||
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
|
||||
|
||||
|
|
88
ietf/person/utils.py
Executable file
88
ietf/person/utils.py
Executable file
|
@ -0,0 +1,88 @@
|
|||
import pprint
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
from ietf.person.models import Person
|
||||
|
||||
def merge_persons(source,target,stream):
|
||||
|
||||
# merge emails
|
||||
for email in source.email_set.all():
|
||||
print >>stream, "Merging email: {}".format(email.address)
|
||||
email.person = target
|
||||
email.save()
|
||||
|
||||
# merge aliases
|
||||
target_aliases = [ a.name for a in target.alias_set.all() ]
|
||||
for alias in source.alias_set.all():
|
||||
if alias.name in target_aliases:
|
||||
alias.delete()
|
||||
else:
|
||||
print >>stream,"Merging alias: {}".format(alias.name)
|
||||
alias.person = target
|
||||
alias.save()
|
||||
|
||||
# merge DocEvents
|
||||
for docevent in source.docevent_set.all():
|
||||
docevent.by = target
|
||||
docevent.save()
|
||||
|
||||
# merge SubmissionEvents
|
||||
for subevent in source.submissionevent_set.all():
|
||||
subevent.by = target
|
||||
subevent.save()
|
||||
|
||||
# merge Messages
|
||||
for message in source.message_set.all():
|
||||
message.by = target
|
||||
message.save()
|
||||
|
||||
# merge Constraints
|
||||
for constraint in source.constraint_set.all():
|
||||
constraint.person = target
|
||||
constraint.save()
|
||||
|
||||
# merge Roles
|
||||
for role in source.role_set.all():
|
||||
role.person = target
|
||||
role.save()
|
||||
|
||||
# merge Nominees
|
||||
for nominee in source.nominee_set.all():
|
||||
target_nominee = target.nominee_set.get(nomcom=nominee.nomcom)
|
||||
if not target_nominee:
|
||||
target_nominee = target.nominee_set.create(nomcom=nominee.nomcom, email=target.email())
|
||||
nominee.nomination_set.all().update(nominee=target_nominee)
|
||||
for fb in nominee.feedback_set.all():
|
||||
fb.nominees.remove(nominee)
|
||||
fb.nominees.add(target_nominee)
|
||||
for np in nominee.nomineeposition_set.all():
|
||||
existing_target_np = target_nominee.nomineeposition_set.filter(position=np.position).first()
|
||||
if existing_target_np:
|
||||
if existing_target_np.state.slug=='pending':
|
||||
existing_target_np.state = np.state
|
||||
existing_target_np.save()
|
||||
np.delete()
|
||||
else:
|
||||
np.nominee=target_nominee
|
||||
np.save()
|
||||
nominee.delete()
|
||||
|
||||
# check for any remaining relationships and delete if none
|
||||
objs = [source]
|
||||
opts = Person._meta
|
||||
user = User.objects.filter(is_superuser=True).first()
|
||||
admin_site = admin.site
|
||||
using = 'default'
|
||||
|
||||
deletable_objects, perms_needed, protected = admin.utils.get_deleted_objects(
|
||||
objs, opts, user, admin_site, using)
|
||||
|
||||
if len(deletable_objects) > 1:
|
||||
print >>stream, "Not Deleting Person: {}({})".format(source.ascii,source.pk)
|
||||
print >>stream, "Related objects remain:"
|
||||
pprint.pprint(deletable_objects[1],stream=stream)
|
||||
|
||||
else:
|
||||
print >>stream, "Deleting Person: {}({})".format(source.ascii,source.pk)
|
||||
source.delete()
|
|
@ -324,7 +324,7 @@ class SubmitTests(TestCase):
|
|||
self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"])
|
||||
self.assertTrue((u"I-D Action: %s" % name) in draft.message_set.order_by("-time")[0].subject)
|
||||
self.assertTrue("Author Name" in unicode(outbox[-3]))
|
||||
self.assertTrue("ietf-announce@" in outbox[-3]['To'])
|
||||
self.assertTrue("i-d-announce@" in outbox[-3]['To'])
|
||||
self.assertTrue("New Version Notification" in outbox[-2]["Subject"])
|
||||
self.assertTrue(name in unicode(outbox[-2]))
|
||||
self.assertTrue("mars" in unicode(outbox[-2]))
|
||||
|
|
11
ietf/templates/nomcom/merge_request.txt
Normal file
11
ietf/templates/nomcom/merge_request.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
The following Person records have been identified by the NomCom chair as potential duplicates.
|
||||
|
||||
The following records:{% for p in duplicate_persons %}
|
||||
{{p.name}} ({{p.id}}) [{{p.email_set.all|join:", "}}]{% endfor %}
|
||||
|
||||
appear to be duplicates of this person (which should be kept)
|
||||
{{primary_person.name}} ({{primary_person.id}}) [{{primary_person.email_set.all|join:", "}}]
|
||||
|
||||
Please verify that these are indeed duplicates, and if so, merge them.
|
||||
|
||||
Thanks in advance.
|
|
@ -33,7 +33,7 @@
|
|||
<li {% if selected == "questionnaire" %}class="active"{% endif %}><a href="{% url "nomcom_private_questionnaire" year %}">Enter questionnaire response</a></li>
|
||||
<li {% if selected == "send_accept_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "accept" %}">Send accept reminder</a></li>
|
||||
<li {% if selected == "send_questionnaire_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "questionnaire" %}">Send questionnaire reminder</a></li>
|
||||
<li {% if selected == "merge" %}class="active"{% endif %}><a href="{% url "nomcom_private_merge" year %}">Merge Email Addresses</a></li>
|
||||
<li {% if selected == "merge" %}class="active"{% endif %}><a href="{% url "nomcom_private_merge" year %}">Request Nominee Merge</a></li>
|
||||
{% endif %}
|
||||
<li role = "presentation" class = "dropdown-header">Nomcom Configuration</li>
|
||||
<li {% if selected == "edit_nomcom" %}class="active"{% endif %}><a href="{% url "nomcom_edit_nomcom" year %}">Edit Settings</a></li>
|
||||
|
@ -45,29 +45,6 @@
|
|||
<li {% if selected == "help" %}class="active"{% endif %}><a href="{% url "nomcom_chair_help" year %}">Configuration Help</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{% comment %}
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Chair <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li {% if selected == "feedback_pending" %}class="active"{% endif %}><a href="{% url "nomcom_view_feedback_pending" year %}">Pending feedback</a></li>
|
||||
<li {% if selected == "feedback_email" %}class="active"{% endif %}><a href="{% url "nomcom_private_feedback_email" year %}">Enter email feedback</a></li>
|
||||
<li {% if selected == "send_accept_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "accept" %}">Send accept reminder</a></li>
|
||||
<li {% if selected == "send_questionnaire_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "questionnaire" %}">Send questionnaire reminder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Edit <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li {% if selected == "edit_nomcom" %}class="active"{% endif %}><a href="{% url "nomcom_edit_nomcom" year %}">Edit Settings</a></li>
|
||||
<li {% if selected == "edit_templates" %}class="active"{% endif %}><a href="{% url "nomcom_list_templates" year %}">Edit Pages</a></li>
|
||||
<li {% if selected == "edit_positions" %}class="active"{% endif %}><a href="{% url "nomcom_list_positions" year %}">Edit Positions</a></li>
|
||||
<li {% if selected == "merge" %}class="active"{% endif %}><a href="{% url "nomcom_private_merge" year %}">Merge Email Addresses</a></li>
|
||||
<li {% if selected == "edit_members" %}class="active"{% endif %}><a href="{% url "nomcom_edit_members" year %}">Edit Nomcom Members</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endcomment %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -1,29 +1,37 @@
|
|||
{% extends "nomcom/nomcom_private_base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block subtitle %} - Merging emails {% endblock %}
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %} - Request Nominee Merge {% endblock %}
|
||||
|
||||
{% block nomcom_content %}
|
||||
{% origin %}
|
||||
<h2>Merging nominee email addresses</h2>
|
||||
<h2>Request Nominee Merge</h2>
|
||||
|
||||
<p>
|
||||
If a nominee has been nominated with multiple email addresses, the nominee will
|
||||
appear multiple times in the nomination list, as the email address is used as
|
||||
the unique identifier for each nominee. In order to permit comments and nominations
|
||||
to be submitted under multiple email addresses, there is a list of secondary email
|
||||
addresses which needs to be kept up-to-date. When nominations of one particular nominee
|
||||
have already been made under different email addresses, the nomination comments from the
|
||||
secondary address also needs to be merged with those under the primary address.
|
||||
The nomination system encourages the community to nominate people by selecting
|
||||
their email address from the set of addresses the tracker already knows. In order
|
||||
to allow a person who does not yet have a datatracker account to be nominated, the
|
||||
system also provides a way for the community to nominate people with a new,
|
||||
previously unknown email address. When this option is chosen, a new Person record
|
||||
is created and associated with the new address.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It doesn't matter particularly which email address is used as primary, as far as the
|
||||
nominee information maintenance goes, but it's probably handier for the nomcom if the
|
||||
primary address is the one which the nominee prefers at the time.
|
||||
Occasionally, this new address should have been associated with an existing person
|
||||
instead. This will happen particularly if the community member uses a slightly incorrect
|
||||
address (such as a typo), or knows the person they want to nominate by a very old or very
|
||||
new address that is not yet in the tracker. When this happens, you can use this form to
|
||||
ask the secretariat to merge the two Person records. The secretariat has a process
|
||||
for verifying that the addresses both belong to the same person, and a tool that
|
||||
can correct the relevant data.
|
||||
</p>
|
||||
|
||||
{% if form %}
|
||||
|
@ -38,3 +46,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'select2/select2.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue