* feat: add pronouns * fix: include migrations * fix: correct daggers on person form. * fix: clean pronouns * feat: add choices to pronouns * feat: show pronouns on public profile * feat: add pronouns to oidc userinfo * fix: move pronouns to new claim. Add tests. * fix: improve html generated by new widget * feat: use a MultiWidget for pronouns * refactor: use two fields on Person for the two types of pronoun entry. * chore: update copyrights
This commit is contained in:
parent
63d80bff4c
commit
8b90ecd4aa
|
@ -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 + ' <a href="#pi" aria-label="!"><i class="bi bi-exclamation-circle"></i></a>')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -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, [])
|
||||
self.assertEqual(logging.root.handlers, [])
|
||||
|
|
|
@ -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. "
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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"]
|
||||
|
|
45
ietf/person/migrations/0023_pronouns.py
Normal file
45
ietf/person/migrations/0023_pronouns.py
Normal file
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
<br>
|
||||
<small class="text-muted">({{ person.ascii }})</small>
|
||||
<span class="text-muted fs-2">({{ person.ascii }})</span>
|
||||
{% endif %}
|
||||
{% if person.pronouns %}
|
||||
<br>
|
||||
<span class="text-muted fs-3">Pronouns: {{person.pronouns}}</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<div class="bio-text">
|
||||
|
|
Loading…
Reference in a new issue