Skip to content

Commit 85f4861

Browse files
committed
Add view for merge person records. Commit ready for merge.
- Legacy-Id: 14862
1 parent f0a4ff2 commit 85f4861

8 files changed

Lines changed: 209 additions & 17 deletions

File tree

ietf/person/forms.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright The IETF Trust 2017, All Rights Reserved
2+
3+
from __future__ import unicode_literals
4+
5+
from django import forms
6+
from ietf.person.models import Person
7+
8+
9+
class MergeForm(forms.Form):
10+
source = forms.IntegerField(label='Source Person ID')
11+
target = forms.IntegerField(label='Target Person ID')
12+
13+
def clean_source(self):
14+
return self.get_person(self.cleaned_data['source'])
15+
16+
def clean_target(self):
17+
return self.get_person(self.cleaned_data['target'])
18+
19+
def get_person(self, pk):
20+
try:
21+
return Person.objects.get(pk=pk)
22+
except Person.DoesNotExist:
23+
raise forms.ValidationError("ID does not exist")

ietf/person/tests.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@
1919
from ietf.person.utils import (merge_persons, determine_merge_order, send_merge_notification,
2020
handle_users, get_extra_primary, dedupe_aliases, move_related_objects, merge_nominees, merge_users)
2121
from ietf.utils.test_data import make_test_data
22-
from ietf.utils.test_utils import TestCase
22+
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
2323
from ietf.utils.mail import outbox, empty_outbox
2424

2525

26-
class PersonTests(TestCase):
26+
def get_person_no_user():
27+
person = PersonFactory()
28+
person.user = None
29+
person.save()
30+
return person
31+
2732

33+
class PersonTests(TestCase):
2834
def test_ajax_search_emails(self):
2935
draft = make_test_data()
3036
person = draft.ad
@@ -87,18 +93,43 @@ def test_duplicate_person_name(self):
8793
Person.objects.create(name="Duplicate Test")
8894
self.assertTrue("possible duplicate" in outbox[0]["Subject"].lower())
8995

96+
def test_merge(self):
97+
url = urlreverse("ietf.person.views.merge")
98+
login_testing_unauthorized(self, "secretary", url)
99+
r = self.client.get(url)
100+
self.assertEqual(r.status_code, 200)
90101

91-
class PersonUtilsTests(TestCase):
92-
def get_person_no_user(self):
93-
person = PersonFactory()
94-
person.user = None
95-
person.save()
96-
return person
102+
def test_merge_with_params(self):
103+
p1 = get_person_no_user()
104+
p2 = PersonFactory()
105+
url = urlreverse("ietf.person.views.merge") + "?source={}&target={}".format(p1.pk, p2.pk)
106+
login_testing_unauthorized(self, "secretary", url)
107+
r = self.client.get(url)
108+
self.assertContains(r, 'retaining login', status_code=200)
109+
110+
def test_merge_with_params_bad_id(self):
111+
url = urlreverse("ietf.person.views.merge") + "?source=1000&target=2000"
112+
login_testing_unauthorized(self, "secretary", url)
113+
r = self.client.get(url)
114+
self.assertContains(r, 'ID does not exist', status_code=200)
97115

116+
def test_merge_post(self):
117+
p1 = get_person_no_user()
118+
p2 = PersonFactory()
119+
url = urlreverse("ietf.person.views.merge")
120+
expected_url = urlreverse("ietf.secr.rolodex.views.view", kwargs={'id': p2.pk})
121+
login_testing_unauthorized(self, "secretary", url)
122+
data = {'source': p1.pk, 'target': p2.pk}
123+
r = self.client.post(url, data, follow=True)
124+
self.assertRedirects(r, expected_url)
125+
self.assertContains(r, 'Merged', status_code=200)
126+
self.assertFalse(Person.objects.filter(pk=p1.pk))
127+
128+
class PersonUtilsTests(TestCase):
98129
def test_determine_merge_order(self):
99-
p1 = self.get_person_no_user()
130+
p1 = get_person_no_user()
100131
p2 = PersonFactory()
101-
p3 = self.get_person_no_user()
132+
p3 = get_person_no_user()
102133
p4 = PersonFactory()
103134

104135
# target has User
@@ -130,12 +161,12 @@ def test_send_merge_notification(self):
130161
self.assertTrue('IETF Datatracker records merged' in outbox[-1]['Subject'])
131162

132163
def test_handle_users(self):
133-
source1 = self.get_person_no_user()
134-
target1 = self.get_person_no_user()
135-
source2 = self.get_person_no_user()
164+
source1 = get_person_no_user()
165+
target1 = get_person_no_user()
166+
source2 = get_person_no_user()
136167
target2 = PersonFactory()
137168
source3 = PersonFactory()
138-
target3 = self.get_person_no_user()
169+
target3 = get_person_no_user()
139170
source4 = PersonFactory()
140171
target4 = PersonFactory()
141172

@@ -224,4 +255,4 @@ def test_merge_users(self):
224255
merge_users(source, target)
225256
self.assertIn(communitylist, target.communitylist_set.all())
226257
self.assertIn(feedback, target.feedback_set.all())
227-
self.assertIn(nomination, target.nomination_set.all())
258+
self.assertIn(nomination, target.nomination_set.all())

ietf/person/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from ietf.utils.urls import url
33

44
urlpatterns = [
5+
url(r'^merge/$', views.merge),
56
url(r'^search/(?P<model_name>(person|email))/$', views.ajax_select2_search),
67
url(r'^(?P<personid>[a-z0-9]+).json$', ajax.person_json),
78
url(r'^(?P<email_or_name>[^/]+)$', views.profile),

ietf/person/views.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import datetime
2+
from StringIO import StringIO
23

4+
from django.contrib import messages
35
from django.db.models import Q
46
from django.http import HttpResponse, Http404
5-
from django.shortcuts import render, get_object_or_404
7+
from django.shortcuts import render, get_object_or_404, redirect
68

79
import debug # pyflakes:ignore
810

11+
from ietf.ietfauth.utils import role_required
912
from ietf.person.models import Email, Person, Alias
1013
from ietf.person.fields import select2_id_name_json
14+
from ietf.person.forms import MergeForm
15+
from ietf.person.utils import handle_users, merge_persons
16+
1117

1218
def ajax_select2_search(request, model_name):
1319
if model_name == "email":
@@ -37,7 +43,7 @@ def ajax_select2_search(request, model_name):
3743
all_emails = request.GET.get("a", "0") == "1"
3844

3945
if model == Email:
40-
objs = objs.exclude(person=None).order_by('person__name')
46+
objs = objs.exclude(person=None).order_by('person__name')
4147
if not all_emails:
4248
objs = objs.filter(active=True)
4349
if only_users:
@@ -66,3 +72,54 @@ def profile(request, email_or_name):
6672
if not persons:
6773
raise Http404
6874
return render(request, 'person/profile.html', {'persons': persons, 'today':datetime.date.today()})
75+
76+
77+
@role_required("Secretariat")
78+
def merge(request):
79+
form = MergeForm()
80+
method = 'get'
81+
change_details = ''
82+
warn_messages = []
83+
source = None
84+
target = None
85+
86+
if request.method == "GET":
87+
form = MergeForm()
88+
if request.GET:
89+
form = MergeForm(request.GET)
90+
if form.is_valid():
91+
source = form.cleaned_data.get('source')
92+
target = form.cleaned_data.get('target')
93+
if source.user and target.user:
94+
warn_messages.append('WARNING: Both Person records have logins. Be sure to specify the record to keep in the Target field.')
95+
if source.user.last_login > target.user.last_login:
96+
warn_messages.append('WARNING: The most recently used login is being deleted!')
97+
change_details = handle_users(source, target, check_only=True)
98+
method = 'post'
99+
else:
100+
method = 'get'
101+
102+
if request.method == "POST":
103+
form = MergeForm(request.POST)
104+
if form.is_valid():
105+
source = form.cleaned_data.get('source')
106+
source_id = source.id
107+
target = form.cleaned_data.get('target')
108+
# Do merge with force
109+
output = StringIO()
110+
success, changes = merge_persons(source, target, file=output)
111+
if success:
112+
messages.success(request, u'Merged {} ({}) to {} ({}). {})'.format(
113+
source.name, source_id, target.name, target.id, changes))
114+
else:
115+
messages.error(request, output)
116+
return redirect('ietf.secr.rolodex.views.view', id=target.pk)
117+
118+
return render(request, 'person/merge.html', {
119+
'form': form,
120+
'method': method,
121+
'change_details': change_details,
122+
'source': source,
123+
'target': target,
124+
'warn_messages': warn_messages,
125+
})

ietf/static/ietf/css/ietf.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,3 +911,10 @@ blockquote {
911911
line-height: 1.0;
912912
cursor: pointer;
913913
}
914+
915+
/* === Person ===================================================== */
916+
917+
.person-info {
918+
margin-bottom: 1.5em;
919+
}
920+

ietf/templates/person/mail/possible_duplicates.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ Please check to see if they represent the same actual person, and if so, merge t
1616
username: {% if person.user %}{{person.user.username}}{% else %}None{% endif %}
1717

1818
{% endfor %} {% endautoescape %}
19+
20+
Merge Link:
21+
{% url "ietf.person.views.merge" %}?source={{ persons.0.pk}}&target={{ persons.1.pk }}

ietf/templates/person/merge.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{% extends "base.html" %}
2+
{# Copyright The IETF Trust 2015, All Rights Reserved #}
3+
{% load staticfiles %}
4+
{% load bootstrap3 %}
5+
6+
{% block title %}Merge Persons{% endblock %}
7+
8+
{% block content %}
9+
10+
<h1>Merge Person Records</h1>
11+
<p>This tool will merge two Person records into one. If both records have logins and you want to retain the one on the left, use the Swap button to swap source and target records.</p>
12+
<form action="" method="{{ method }}">{% if method == 'post' %}{% csrf_token %}{% endif %}
13+
<div class="row">
14+
<div class="form-group">
15+
<div class="col-md-6">
16+
{% bootstrap_field form.source %}
17+
{% if source %}
18+
{% with person=source %}
19+
{% include "person/person_info.html" %}
20+
{% endwith %}
21+
{% endif %}
22+
</div>
23+
24+
<div class="col-md-6">
25+
{% bootstrap_field form.target %}
26+
{% if target %}
27+
{% with person=target %}
28+
{% include "person/person_info.html" %}
29+
{% endwith %}
30+
{% endif %}
31+
</div>
32+
</div>
33+
</div>
34+
{% if change_details %}
35+
<div class="alert alert-info" role="alert">{{ change_details }}</div>
36+
{% endif %}
37+
{% if warn_messages %}
38+
{% for message in warn_messages %}
39+
<div class="alert alert-warning" role="alert">{{ message }}</div>
40+
{% endfor %}
41+
{% endif %}
42+
{% if method == 'post' %}
43+
<a class="btn btn-default" href="{% url 'ietf.person.views.merge' %}?source={{ target.pk }}&target={{ source.pk }}" role="button">Swap</a>
44+
{% endif %}
45+
<button type="submit" class="btn btn-default">{% if method == 'post' %}Merge{% else %}Submit{% endif %}</button>
46+
</form>
47+
48+
{% endblock %}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<div class="person-info">
2+
<div class="row">
3+
<div class="col-md-2">Name:</div><div class="col-md-10">{{ person.name }}</div>
4+
</div>
5+
<div class="row">
6+
<div class="col-md-2">Address:</div><div class="col-md-10">{{ person.address }}</div>
7+
</div>
8+
<div class="row">
9+
<div class="col-md-2">Affiliation:</div><div class="col-md-10">{{ person.affiliation}}</div>
10+
</div>
11+
<div class="row">
12+
<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>
13+
</div>
14+
{% for email in person.email_set.all %}
15+
<div class="row">
16+
<div class="col-md-2">{% if forloop.first %}Email{{ person.email_set.count|pluralize }}:{% endif %}</div><div class="col-md-10">{{ email.address }}</div>
17+
</div>
18+
{% endfor %}
19+
<div class="row">
20+
<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>
21+
</div>
22+
</div>

0 commit comments

Comments
 (0)