Skip to content

Commit 2548004

Browse files
Change DUA signing from full name to initials
1 parent d1c9bac commit 2548004

File tree

5 files changed

+80
-38
lines changed

5 files changed

+80
-38
lines changed

physionet-django/project/forms.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,27 +1111,28 @@ class Meta:
11111111

11121112
class DUASignatureForm(forms.Form):
11131113
"""
1114-
Form for signing Data Use Agreement by typing full name.
1114+
Form for signing Data Use Agreement by typing initials.
11151115
"""
1116-
full_name = forms.CharField(
1116+
initials = forms.CharField(
11171117
widget=forms.TextInput(attrs={
11181118
'class': 'form-control',
1119-
'placeholder': 'Type your full name',
1119+
'placeholder': 'Type your initials',
11201120
}),
1121-
label='Please sign your full name to indicate your consent',
1121+
label='Please type your initials to indicate your consent',
11221122
)
11231123

11241124
def __init__(self, user, *args, **kwargs):
11251125
super().__init__(*args, **kwargs)
11261126
self.user = user
1127-
self.fields['full_name'].help_text = (
1128-
f'Please type your full name exactly as: {user.get_full_name()}'
1127+
expected_initials = DUASignature.get_user_initials(user)
1128+
self.fields['initials'].help_text = (
1129+
f'Please type your initials exactly as: {expected_initials}'
11291130
)
11301131

1131-
def clean_full_name(self):
1132-
full_name = self.cleaned_data.get('full_name', '').strip()
1133-
DUASignature.validate_signature_name(self.user, full_name)
1134-
return full_name
1132+
def clean_initials(self):
1133+
initials = self.cleaned_data.get('initials', '').strip()
1134+
DUASignature.validate_signature_initials(self.user, initials)
1135+
return initials
11351136

11361137

11371138
class DataAccessRequestForm(forms.ModelForm):

physionet-django/project/modelcomponents/access.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import unicodedata
12
from datetime import timedelta
23
from enum import IntEnum
34

@@ -44,13 +45,31 @@ class Meta:
4445
default_permissions = ()
4546

4647
@staticmethod
47-
def validate_signature_name(user, full_name):
48-
if not full_name or not full_name.strip():
49-
raise ValidationError('You must enter your full name to sign the agreement.')
50-
expected_name = user.get_full_name()
51-
if full_name.strip().lower() != expected_name.lower():
48+
def get_user_initials(user):
49+
"""Get initials from user's profile name."""
50+
initials = ''
51+
if user.profile.first_names:
52+
for name in user.profile.first_names.split():
53+
if name:
54+
initials += name[0].upper()
55+
if user.profile.last_name:
56+
initials += user.profile.last_name[0].upper()
57+
return initials
58+
59+
@staticmethod
60+
def normalize_for_comparison(text):
61+
"""Normalize text for Unicode-aware case-insensitive comparison."""
62+
return unicodedata.normalize('NFKD', text.casefold())
63+
64+
@staticmethod
65+
def validate_signature_initials(user, initials):
66+
if not initials or not initials.strip():
67+
raise ValidationError('You must enter your initials to sign the agreement.')
68+
expected_initials = DUASignature.get_user_initials(user)
69+
if DUASignature.normalize_for_comparison(initials.strip()) != \
70+
DUASignature.normalize_for_comparison(expected_initials):
5271
raise ValidationError(
53-
f'The name entered does not match your profile name: {expected_name}'
72+
f'The initials entered do not match. Please enter: {expected_initials}'
5473
)
5574

5675

physionet-django/project/templates/project/sign_dua.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ <h2>{{ project.dua.name }}</h2>
1919
<form method="POST">
2020
{% csrf_token %}
2121
<div class="form-group">
22-
<label for="id_full_name">{{ form.full_name.label }}</label>
23-
{{ form.full_name }}
24-
{% if form.full_name.help_text %}
25-
<small class="form-text text-muted">{{ form.full_name.help_text }}</small>
22+
<label for="id_initials">{{ form.initials.label }}</label>
23+
{{ form.initials }}
24+
{% if form.initials.help_text %}
25+
<small class="form-text text-muted">{{ form.initials.help_text }}</small>
2626
{% endif %}
27-
{% if form.full_name.errors %}
28-
<div class="text-danger">{{ form.full_name.errors.0 }}</div>
27+
{% if form.initials.errors %}
28+
<div class="text-danger">{{ form.initials.errors.0 }}</div>
2929
{% endif %}
3030
</div>
3131
<p class="text-center">

physionet-django/project/test_s3.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
from project.models import (
2525
AWS,
26+
DUASignature,
2627
PublishedProject,
2728
)
2829
from user.models import (
@@ -280,9 +281,10 @@ def test_controlled_access_points(self):
280281

281282
for user in add_aws_users + add_nonaws_users:
282283
self.client.force_login(user)
284+
initials = DUASignature.get_user_initials(user)
283285
self.client.post(
284286
reverse('sign_dua', args=(project.slug, project.version)),
285-
data={'agree': '', 'full_name': user.get_full_name()},
287+
data={'agree': '', 'initials': initials},
286288
)
287289
self.assertTrue(can_view_project_files(project, user))
288290

physionet-django/project/test_views.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ def test_credentialed(self):
735735
# Sign the dua and get file again
736736
response = self.client.post(reverse('sign_dua',
737737
args=(project.slug, project.version,)),
738-
data={'agree': '', 'full_name': 'Roger Greenwood Mark'})
738+
data={'agree': '', 'initials': 'RGM'})
739739
response = self.client.get(reverse(
740740
'serve_published_project_file',
741741
args=(project.slug, project.version, 'SHA256SUMS.txt')))
@@ -952,7 +952,7 @@ def test_serve_file(self):
952952
self.client.login(username='rgmark@mit.edu', password='Tester11!')
953953
response = self.client.post(
954954
reverse('sign_dua', args=(project.slug, project.version,)),
955-
data={'agree': '', 'full_name': 'Roger Greenwood Mark'})
955+
data={'agree': '', 'initials': 'RGM'})
956956

957957
response = self.client.get(url + 'foo/')
958958
self.assertEqual(response.status_code, 200)
@@ -967,46 +967,46 @@ def test_serve_file(self):
967967

968968
class TestDUASignatureValidation(TestMixin):
969969
"""
970-
Test DUA signing with full name validation.
970+
Test DUA signing with initials validation.
971971
"""
972972

973-
def test_sign_dua_with_correct_name(self):
974-
"""User can sign DUA when typing their correct full name."""
973+
def test_sign_dua_with_correct_initials(self):
974+
"""User can sign DUA when typing their correct initials."""
975975
project = PublishedProject.objects.get(slug='demoeicu', version='2.0.0')
976976
self.client.login(username='rgmark@mit.edu', password='Tester11!')
977977

978978
response = self.client.post(
979979
reverse('sign_dua', args=(project.slug, project.version)),
980-
data={'agree': '', 'full_name': 'Roger Greenwood Mark'}
980+
data={'agree': '', 'initials': 'RGM'}
981981
)
982982
self.assertEqual(response.status_code, 200)
983983
self.assertTrue(DUASignature.objects.filter(
984984
user__email='rgmark@mit.edu', project=project
985985
).exists())
986986

987-
def test_sign_dua_with_wrong_name(self):
988-
"""User cannot sign DUA when typing wrong name."""
987+
def test_sign_dua_with_wrong_initials(self):
988+
"""User cannot sign DUA when typing wrong initials."""
989989
project = PublishedProject.objects.get(slug='demoeicu', version='2.0.0')
990990
self.client.login(username='rgmark@mit.edu', password='Tester11!')
991991

992992
response = self.client.post(
993993
reverse('sign_dua', args=(project.slug, project.version)),
994-
data={'agree': '', 'full_name': 'Wrong Name'}
994+
data={'agree': '', 'initials': 'XYZ'}
995995
)
996996
self.assertEqual(response.status_code, 200)
997997
self.assertFalse(DUASignature.objects.filter(
998998
user__email='rgmark@mit.edu', project=project
999999
).exists())
1000-
self.assertContains(response, 'does not match your profile name')
1000+
self.assertContains(response, 'do not match')
10011001

1002-
def test_sign_dua_with_empty_name(self):
1003-
"""User cannot sign DUA without entering their name."""
1002+
def test_sign_dua_with_empty_initials(self):
1003+
"""User cannot sign DUA without entering their initials."""
10041004
project = PublishedProject.objects.get(slug='demoeicu', version='2.0.0')
10051005
self.client.login(username='rgmark@mit.edu', password='Tester11!')
10061006

10071007
response = self.client.post(
10081008
reverse('sign_dua', args=(project.slug, project.version)),
1009-
data={'agree': '', 'full_name': ''}
1009+
data={'agree': '', 'initials': ''}
10101010
)
10111011
self.assertEqual(response.status_code, 200)
10121012
self.assertFalse(DUASignature.objects.filter(
@@ -1015,20 +1015,40 @@ def test_sign_dua_with_empty_name(self):
10151015
self.assertContains(response, 'This field is required')
10161016

10171017
def test_sign_dua_case_insensitive(self):
1018-
"""Name validation is case-insensitive."""
1018+
"""Initials validation is case-insensitive."""
10191019
project = PublishedProject.objects.get(slug='demoeicu', version='2.0.0')
10201020
self.client.login(username='rgmark@mit.edu', password='Tester11!')
10211021

10221022
response = self.client.post(
10231023
reverse('sign_dua', args=(project.slug, project.version)),
1024-
data={'agree': '', 'full_name': 'roger greenwood mark'}
1024+
data={'agree': '', 'initials': 'rgm'}
10251025
)
10261026
self.assertEqual(response.status_code, 200)
10271027
self.assertTrue(DUASignature.objects.filter(
10281028
user__email='rgmark@mit.edu', project=project
10291029
).exists())
10301030

10311031

1032+
class TestDUASignatureNormalization(TestCase):
1033+
"""
1034+
Test Unicode normalization for initials comparison.
1035+
"""
1036+
1037+
def test_normalize_for_comparison(self):
1038+
"""Test that normalize_for_comparison handles Unicode properly."""
1039+
normalize = DUASignature.normalize_for_comparison
1040+
1041+
# Case insensitivity
1042+
self.assertEqual(normalize('ABC'), normalize('abc'))
1043+
1044+
# German sharp S (ß) casefolds to 'ss'
1045+
self.assertEqual(normalize('ß'), normalize('ss'))
1046+
1047+
# Different Unicode representations of same character
1048+
# é as single character vs e + combining acute accent
1049+
self.assertEqual(normalize('é'), normalize('é'))
1050+
1051+
10321052
class TestState(TestMixin):
10331053
"""
10341054
Test that all objects are in their intended states, during and

0 commit comments

Comments
 (0)