Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ietf/ietfauth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,9 @@ def confirm_account(request, auth):
if not person:
name = form.cleaned_data["name"]
ascii = form.cleaned_data["ascii"]
person = Person.objects.create(user=user,
name=name,
ascii=ascii)

person = Person.objects.create(user=user, name=name, ascii=ascii)
person.uuids.create(person=person)

for name in set([ person.name, person.ascii, person.plain_name(), person.plain_ascii(), ]):
Alias.objects.create(person=person, name=name)
Expand Down
17 changes: 15 additions & 2 deletions ietf/person/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from django import forms

from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent, PersonExtResource
from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, \
PersonApiKeyEvent, PersonExtResource, PersonUUID
from ietf.person.name import name_parts

from ietf.utils.admin import SaferStackedInline, SaferTabularInline
Expand All @@ -29,6 +30,18 @@ class AliasAdmin(admin.ModelAdmin):
class AliasInline(SaferStackedInline):
model = Alias


class PersonUUIDAdmin(admin.ModelAdmin):
list_display = ["uuid"]
raw_id_fields = ["person"]
admin.site.register(PersonUUID, PersonUUIDAdmin)


class PersonUUIDInline(SaferStackedInline):
model = PersonUUID
extra = 0


class PersonAdmin(simple_history.admin.SimpleHistoryAdmin):
def plain_name(self, obj):
if obj.plain:
Expand All @@ -41,7 +54,7 @@ def plain_name(self, obj):
readonly_fields = ("name_from_draft", )
search_fields = ["name", "ascii"]
raw_id_fields = ["user"]
inlines = [ EmailInline, AliasInline, ]
inlines = [ EmailInline, AliasInline, PersonUUIDInline]
# actions = None
admin.site.register(Person, PersonAdmin)

Expand Down
12 changes: 11 additions & 1 deletion ietf/person/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@

import debug # pyflakes:ignore

from ietf.person.models import Person, Alias, Email, PersonalApiKey, PersonApiKeyEvent, PERSON_API_KEY_ENDPOINTS
from ietf.person.models import Person, Alias, Email, PersonalApiKey, PersonApiKeyEvent, \
PERSON_API_KEY_ENDPOINTS, PersonUUID
from ietf.person.name import normalize_name, unidecode_name


Expand Down Expand Up @@ -64,12 +65,21 @@ def set_password(obj, create, extracted, **kwargs): # pylint: disable=no-self-ar
obj.set_password( '%s+password' % obj.username ) # pylint: disable=no-value-for-parameter
obj.save()


class PersonUUIDFactory(factory.django.DjangoModelFactory):
person = factory.SubFactory("ietf.person.factories.PersonFactory")

class Meta:
model = PersonUUID


class PersonFactory(factory.django.DjangoModelFactory):
class Meta:
model = Person
skip_postgeneration_save = True

user = factory.SubFactory(UserFactory)
uuid = factory.RelatedFactory("ietf.person.factories.PersonUUIDFactory", "person")
name = factory.LazyAttribute(lambda p: normalize_name('%s %s'%(p.user.first_name, p.user.last_name)))
# Some i18n names, e.g., "शिला के.सी." have a dot at the end that is also part of the ASCII, e.g., "Shilaa Kesii."
# That trailing dot breaks extract_authors(). Avoid this issue by stripping the dot from the ASCII.
Expand Down
47 changes: 47 additions & 0 deletions ietf/person/migrations/0006_personuuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright The IETF Trust 2026, All Rights Reserved

from django.db import migrations, models
import django.db.models.deletion
import ietf.person.models


def forward(apps, schema_editor):
Person = apps.get_model("person", "Person")
for person in Person.objects.all():
person.uuids.create(person=person)


def reverse(apps, schema_editor):
pass


class Migration(migrations.Migration):
dependencies = [
("person", "0005_alter_historicalperson_pronouns_selectable_and_more"),
]

operations = [
migrations.CreateModel(
name="PersonUUID",
fields=[
(
"uuid",
models.UUIDField(
default=ietf.person.models.unused_person_uuid,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"person",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="uuids",
to="person.person",
),
),
],
),
migrations.RunPython(forward, reverse),
]
20 changes: 20 additions & 0 deletions ietf/person/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ def name_character_validator(value):
)


def unused_person_uuid():
MAX_ATTEMPTS = 50 # ludicrously large
for _ in range(MAX_ATTEMPTS):
candidate = uuid.uuid4()
if not PersonUUID.objects.filter(uuid=candidate).exists():
return candidate
raise RuntimeError("Unable to generate unused UUID")


class PersonUUID(models.Model):
"""Surrogate key for a Person"""
uuid = models.UUIDField(primary_key=True, editable=False, default=unused_person_uuid)
person = models.ForeignKey(
"person.Person", related_name="uuids", on_delete=models.PROTECT
)

def __str__(self):
return str(self.uuid)


class Person(models.Model):
history = HistoricalRecords()
user = OneToOneField(User, blank=True, null=True, on_delete=models.SET_NULL)
Expand Down
32 changes: 32 additions & 0 deletions ietf/person/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,31 @@ def test_person_profile_without_email(self):
r = self.client.get(url)
self.assertContains(r, person.name, status_code=200)

def test_person_profile_by_uuid(self):
person_a = PersonFactory(name="A Fine Person")
uuid_a = person_a.uuids.first()
url_a = urlreverse("ietf.person.views.profile_by_uuid", kwargs={"uuid": uuid_a.uuid})

person_b = PersonFactory(name="Brilliant Person")
uuid_b = person_b.uuids.first()
url_b = urlreverse("ietf.person.views.profile_by_uuid", kwargs={"uuid": uuid_b.uuid})

r = self.client.get(url_a)
self.assertContains(r, person_a.name)
self.assertNotContains(r, person_b.name)

r = self.client.get(url_b)
self.assertNotContains(r, person_a.name)
self.assertContains(r, person_b.name)

# move the UUID from b to a...
uuid_b.person = person_a
uuid_b.save()
# ... and see that the view returns the other person's values
r = self.client.get(url_a)
self.assertContains(r, person_a.name)
self.assertNotContains(r, person_b.name)

def test_case_insensitive(self):
# Case insensitive seach
person = PersonFactory(name="Test Person")
Expand Down Expand Up @@ -392,9 +417,12 @@ def test_merge_persons(self):
request = HttpRequest()
request.user = user
source = PersonFactory()
source.uuids.create() # give them an extra
target = PersonFactory()
mars = RoleFactory(name_id='chair',group__acronym='mars').group
source_id = source.pk
source_uuids = set(source.uuids.values_list("uuid", flat=True))
target_uuids = set(target.uuids.values_list("uuid", flat=True))
source_email = source.email_set.first()
source_alias = source.alias_set.first()
source_user = source.user
Expand All @@ -413,6 +441,10 @@ def test_merge_persons(self):
self.assertIn(nomination, target.nomination_set.all())
self.assertFalse(Person.objects.filter(id=source_id))
self.assertFalse(source_user.is_active)
self.assertEqual(
set(target.uuids.values_list("uuid", flat=True)),
source_uuids | target_uuids,
)

def test_merge_persons_reviewer_settings(self):
secretariat_role = RoleFactory(group__acronym='secretariat', name_id='secr')
Expand Down
1 change: 1 addition & 0 deletions ietf/person/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
url(r'^merge/send_request/?$', views.send_merge_request),
url(r'^search/(?P<model_name>(person|email))/$', views.ajax_select2_search),
url(r'^(?P<personid>[0-9]+)/email.json$', ajax.person_email_json),
url(r'^(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$', views.profile_by_uuid),
url(r'^(?P<email_or_name>[^/]+)$', views.profile),
url(r'^(?P<email_or_name>[^/]+)/photo/?$', views.photo),
]
3 changes: 3 additions & 0 deletions ietf/person/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def merge_persons(request, source, target, file=sys.stdout, verbose=False):
move_related_objects(source, target, file=file, verbose=verbose)
dedupe_aliases(target)

# move UUIDs
source.uuids.update(person=target)

# copy other attributes
for field in ('ascii','ascii_short', 'biography', 'photo', 'photo_thumb', 'name_from_draft'):
if getattr(source,field) and not getattr(target,field):
Expand Down
9 changes: 8 additions & 1 deletion ietf/person/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from django.contrib import messages
from django.db.models import Q
from django.http import HttpResponse, Http404
from django.shortcuts import render, redirect
from django.shortcuts import render, redirect, get_object_or_404
from django.template.loader import render_to_string
from django.utils import timezone

Expand Down Expand Up @@ -77,6 +77,13 @@ def profile(request, email_or_name):
return render(request, 'person/profile.html', {'persons': persons, 'today': timezone.now()})


def profile_by_uuid(request, uuid):
person = get_object_or_404(Person, uuids=uuid)
return render(
request, "person/profile.html", {"persons": [person], "today": timezone.now()}
)


def photo(request, email_or_name):
persons = lookup_persons(email_or_name)
if len(persons) > 1:
Expand Down
Loading