diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py
index fe56c7ca9..851ad02f8 100644
--- a/ietf/ietfauth/forms.py
+++ b/ietf/ietfauth/forms.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2011-2020, All Rights Reserved
+# Copyright The IETF Trust 2011-2022, All Rights Reserved
# -*- coding: utf-8 -*-
@@ -118,9 +118,11 @@ 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', ]:
+ self.fields['pronouns_selectable'] = forms.MultipleChoiceField(label='Pronouns', choices = [(option, option) for option in ["he/him", "she/her", "they/them"]], widget=forms.CheckboxSelectMultiple, required=False)
+
+ for f in ['name', 'ascii', 'ascii_short', 'biography', 'photo', 'photo_thumb', 'pronouns_selectable']:
if f in self.fields:
- self.fields[f].label += ' \u2020'
+ self.fields[f].label = mark_safe(self.fields[f].label + ' ')
self.unidecoded_ascii = False
@@ -131,6 +133,7 @@ def get_person_form(*args, **kwargs):
self.data["ascii"] = reconstructed_name
self.unidecoded_ascii = name != reconstructed_name
+
def clean_name(self):
name = self.cleaned_data.get("name") or ""
prevent_at_symbol(name)
@@ -158,11 +161,17 @@ def get_person_form(*args, **kwargs):
self.cleaned_data.get('name') != person.name_from_draft
or self.cleaned_data.get('ascii') != person.name_from_draft
or self.cleaned_data.get('biography')
+ or self.cleaned_data.get('pronouns_selectable')
+ or self.cleaned_data.get('pronouns_freetext')
)
if consent == False and require_consent:
raise forms.ValidationError("In order to modify your profile with data that require consent, you must permit the IETF to use the uploaded data.")
return consent
+ def clean(self):
+ if self.cleaned_data.get("pronouns_selectable") and self.cleaned_data.get("pronouns_freetext"):
+ self.add_error("pronouns_freetext", "Either select from the pronoun checkboxes or provide a custom value, but not both")
+
return PersonForm(*args, **kwargs)
diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py
index 108baab88..14c252283 100644
--- a/ietf/ietfauth/tests.py
+++ b/ietf/ietfauth/tests.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2009-2020, All Rights Reserved
+# Copyright The IETF Trust 2009-2022, All Rights Reserved
# -*- coding: utf-8 -*-
@@ -248,6 +248,7 @@ class IetfAuthTests(TestCase):
"plain": "",
"ascii": "Test Name",
"ascii_short": "T. Name",
+ "pronouns_freetext": "foo/bar",
"affiliation": "Test Org",
"active_emails": email_address,
"consent": True,
@@ -319,6 +320,40 @@ class IetfAuthTests(TestCase):
q = PyQuery(r.content)
self.assertEqual(len(q('[name="action"][value="confirm"]')), 0)
+ pronoundish = base_data.copy()
+ pronoundish["pronouns_freetext"] = "baz/boom"
+ r = self.client.post(url, pronoundish)
+ self.assertEqual(r.status_code, 200)
+ person = Person.objects.get(user__username=username)
+ self.assertEqual(person.pronouns_freetext,"baz/boom")
+ pronoundish["pronouns_freetext"]=""
+ r = self.client.post(url, pronoundish)
+ self.assertEqual(r.status_code, 200)
+ person = Person.objects.get(user__username=username)
+ self.assertEqual(person.pronouns_freetext, None)
+
+ pronoundish = base_data.copy()
+ del pronoundish["pronouns_freetext"]
+ pronoundish["pronouns_selectable"] = []
+ r = self.client.post(url, pronoundish)
+ self.assertEqual(r.status_code, 200)
+ person = Person.objects.get(user__username=username)
+ self.assertEqual(person.pronouns_selectable,[])
+ pronoundish["pronouns_selectable"] = ['he/him','she/her']
+ r = self.client.post(url, pronoundish)
+ self.assertEqual(r.status_code, 200)
+ person = Person.objects.get(user__username=username)
+ self.assertEqual(person.pronouns_selectable,['he/him','she/her'])
+ self.assertEqual(person.pronouns(),"he/him, she/her")
+
+ # Can't have both selectables and freetext
+ pronoundish["pronouns_freetext"] = "foo/bar/baz"
+ r = self.client.post(url, pronoundish)
+ self.assertContains(r, 'but not both' ,status_code=200)
+ q = PyQuery(r.content)
+ self.assertTrue(len(q("form div.invalid-feedback")) == 1)
+
+
# change role email
role = Role.objects.create(
person=Person.objects.get(user__username=username),
@@ -854,7 +889,7 @@ class OpenIDConnectTests(TestCase):
client.store_registration_info(client_reg)
# Get a user for which we want to get access
- person = PersonFactory(with_bio=True)
+ person = PersonFactory(with_bio=True, pronouns_freetext="foo/bar")
active_group = RoleFactory(name_id='chair', person=person).group
closed_group = RoleFactory(name_id='chair', person=person, group__state_id='conclude').group
# an additional email
@@ -872,7 +907,7 @@ class OpenIDConnectTests(TestCase):
session["nonce"] = rndstr()
args = {
"response_type": "code",
- "scope": ['openid', 'profile', 'email', 'roles', 'registration', 'dots' ],
+ "scope": ['openid', 'profile', 'email', 'roles', 'registration', 'dots', 'pronouns' ],
"nonce": session["nonce"],
"redirect_uri": redirect_uris[0],
"state": session["state"]
@@ -920,7 +955,7 @@ class OpenIDConnectTests(TestCase):
# Get userinfo, check keys present
userinfo = client.do_user_info_request(state=params["state"], scope=args['scope'])
- for key in [ 'email', 'family_name', 'given_name', 'meeting', 'name', 'roles',
+ for key in [ 'email', 'family_name', 'given_name', 'meeting', 'name', 'pronouns', 'roles',
'ticket_type', 'reg_type', 'affiliation', 'picture', 'dots', ]:
self.assertIn(key, userinfo)
self.assertTrue(userinfo[key])
@@ -957,4 +992,4 @@ class OpenIDConnectTests(TestCase):
# logging.debug() instead of logger.debug(), which results in setting a root
# handler, causing later logging to become visible even if that wasn't intended.
# Fail here if that happens.
- self.assertEqual(logging.root.handlers, [])
\ No newline at end of file
+ self.assertEqual(logging.root.handlers, [])
diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py
index 280a92574..8d2c8a016 100644
--- a/ietf/ietfauth/utils.py
+++ b/ietf/ietfauth/utils.py
@@ -258,6 +258,9 @@ class OidcExtraScopeClaims(oidc_provider.lib.claims.ScopeClaims):
dots = get_dots(self.user.person)
return { 'dots': dots }
+ def scope_pronouns(self):
+ return { 'pronouns': self.user.person.pronouns() }
+
info_registration = (
"IETF Meeting Registration Info",
"Access to public IETF meeting registration information for the current meeting. "
diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py
index 892e3c646..fb6c560f1 100644
--- a/ietf/ietfauth/views.py
+++ b/ietf/ietfauth/views.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2007-2020, All Rights Reserved
+# Copyright The IETF Trust 2007-2022, All Rights Reserved
# -*- coding: utf-8 -*-
#
# Portions Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
diff --git a/ietf/person/admin.py b/ietf/person/admin.py
index 0bb17b0b6..3569c0334 100644
--- a/ietf/person/admin.py
+++ b/ietf/person/admin.py
@@ -1,3 +1,4 @@
+# Copyright The IETF Trust 2022, All Rights Reserved
from django.contrib import admin
import simple_history
@@ -35,7 +36,7 @@ class PersonAdmin(simple_history.admin.SimpleHistoryAdmin):
prefix, first, middle, last, suffix = name_parts(obj.name)
return "%s %s" % (first, last)
list_display = ["name", "short", "plain_name", "time", "user", ]
- fields = ("user", "time", "name", "plain", "name_from_draft", "ascii", "ascii_short", "biography", "photo", "photo_thumb", "consent",)
+ fields = ("user", "time", "name", "plain", "name_from_draft", "ascii", "ascii_short", "pronouns_selectable", "pronouns_freetext", "biography", "photo", "photo_thumb", "consent",)
readonly_fields = ("name_from_draft", )
search_fields = ["name", "ascii"]
raw_id_fields = ["user"]
diff --git a/ietf/person/migrations/0023_pronouns.py b/ietf/person/migrations/0023_pronouns.py
new file mode 100644
index 000000000..e6fc8eac1
--- /dev/null
+++ b/ietf/person/migrations/0023_pronouns.py
@@ -0,0 +1,45 @@
+# Copyright The IETF Trust 2022, All Rights Reserved
+# Generated by Django 2.2.28 on 2022-06-17 15:09
+
+from django.db import migrations, models
+import jsonfield.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('person', '0022_auto_20220513_1456'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='historicalperson',
+ name='pronouns_freetext',
+ field=models.CharField(blank=True, help_text='Optionally provide your personal pronouns. These will be displayed on your public profile page and alongside your name in Meetecho and, in future, other systems. Select any number of the checkboxes OR provide a custom string up to 30 characters.', max_length=30, null=True, verbose_name=' '),
+ ),
+ migrations.AddField(
+ model_name='historicalperson',
+ name='pronouns_selectable',
+ field=jsonfield.fields.JSONCharField(blank=True, default=list, max_length=120, null=True, verbose_name='Pronouns'),
+ ),
+ migrations.AddField(
+ model_name='person',
+ name='pronouns_freetext',
+ field=models.CharField(blank=True, help_text='Optionally provide your personal pronouns. These will be displayed on your public profile page and alongside your name in Meetecho and, in future, other systems. Select any number of the checkboxes OR provide a custom string up to 30 characters.', max_length=30, null=True, verbose_name=' '),
+ ),
+ migrations.AddField(
+ model_name='person',
+ name='pronouns_selectable',
+ field=jsonfield.fields.JSONCharField(blank=True, default=list, max_length=120, null=True, verbose_name='Pronouns'),
+ ),
+ migrations.AlterField(
+ model_name='historicalperson',
+ name='consent',
+ field=models.BooleanField(default=None, null=True, verbose_name='I hereby give my consent to the use of the personal details I have provided (photo, bio, name, pronouns, email) within the IETF Datatracker'),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='consent',
+ field=models.BooleanField(default=None, null=True, verbose_name='I hereby give my consent to the use of the personal details I have provided (photo, bio, name, pronouns, email) within the IETF Datatracker'),
+ ),
+ ]
diff --git a/ietf/person/models.py b/ietf/person/models.py
index 6094ac214..143b8dd08 100644
--- a/ietf/person/models.py
+++ b/ietf/person/models.py
@@ -1,10 +1,11 @@
-# Copyright The IETF Trust 2010-2020, All Rights Reserved
+# Copyright The IETF Trust 2010-2022, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import email.utils
import email.header
+import jsonfield
import uuid
from hashids import Hashids
@@ -46,11 +47,13 @@ class Person(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).")
+ pronouns_selectable = jsonfield.JSONCharField("Pronouns", max_length=120, blank=True, null=True, default=list )
+ pronouns_freetext = models.CharField(" ", max_length=30, null=True, blank=True, help_text="Optionally provide your personal pronouns. These will be displayed on your public profile page and alongside your name in Meetecho and, in future, other systems. Select any number of the checkboxes OR provide a custom string up to 30 characters.")
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.BooleanField("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)
+ consent = models.BooleanField("I hereby give my consent to the use of the personal details I have provided (photo, bio, name, pronouns, email) within the IETF Datatracker", null=True, default=None)
def __str__(self):
return self.plain_name()
@@ -106,6 +109,13 @@ class Person(models.Model):
return name_parts(self.name)[1]
def aliases(self):
return [ str(a) for a in self.alias_set.all() ]
+
+ def pronouns(self):
+ if self.pronouns_selectable:
+ return ", ".join(self.pronouns_selectable)
+ else:
+ return self.pronouns_freetext
+
def role_email(self, role_name, group=None):
"""Lookup email for role for person, optionally on group which
may be an object or the group acronym."""
@@ -202,6 +212,8 @@ class Person(models.Model):
if not email.origin.split(':')[0] in ['author', 'role', 'reviewer', 'liaison', 'shepherd', ]:
needs_consent.append("email address(es)")
break
+ if self.pronouns_freetext or self.pronouns_selectable:
+ needs_consent.append("pronouns")
return needs_consent
def save(self, *args, **kwargs):
diff --git a/ietf/person/tests.py b/ietf/person/tests.py
index bd754194d..59228a795 100644
--- a/ietf/person/tests.py
+++ b/ietf/person/tests.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2014-2020, All Rights Reserved
+# Copyright The IETF Trust 2014-2022, All Rights Reserved
# -*- coding: utf-8 -*-
@@ -80,7 +80,7 @@ class PersonTests(TestCase):
self.assertTrue(primary.address in person.formatted_email())
def test_person_profile(self):
- person = PersonFactory(with_bio=True)
+ person = PersonFactory(with_bio=True,pronouns_freetext="foo/bar")
self.assertTrue(person.photo is not None)
self.assertTrue(person.photo.name is not None)
@@ -91,6 +91,7 @@ class PersonTests(TestCase):
#debug.show('person.plain_name()')
#debug.show('person.photo_name()')
self.assertContains(r, person.photo_name(), status_code=200)
+ self.assertContains(r, "foo/bar")
q = PyQuery(r.content)
self.assertIn("Photo of %s"%person.name, q("div.bio-text img").attr("alt"))
diff --git a/ietf/templates/person/profile.html b/ietf/templates/person/profile.html
index 9ff6ba694..6732804aa 100644
--- a/ietf/templates/person/profile.html
+++ b/ietf/templates/person/profile.html
@@ -1,5 +1,5 @@
{% extends "base.html" %}
-{# Copyright The IETF Trust 2015, All Rights Reserved #}
+{# Copyright The IETF Trust 2015-2022, All Rights Reserved #}
{% load origin %}
{% load markup_tags %}
{% load static %}
@@ -22,7 +22,11 @@
{{ person.name }}
{% if person.ascii != person.name %}
- ({{ person.ascii }})
+ ({{ person.ascii }})
+ {% endif %}
+ {% if person.pronouns %}
+
+ Pronouns: {{person.pronouns}}
{% endif %}