Skip to content
Open
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
33 changes: 21 additions & 12 deletions docassemble/AssemblyLine/al_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -2188,10 +2188,10 @@ def familiar(

In order, it will try to use:

* just the first name
* the first name and suffix
* the first and middle name
* the first and last name
* just the preferred name (if set) or first name
* the preferred/first name and suffix
* the preferred/first and middle name
* the preferred/first and last name
* the full name
* the default value, e.g., "the minor", if provided
* the full name
Expand Down Expand Up @@ -2221,33 +2221,42 @@ def familiar(
if unique_names is None:
unique_names = []

# Use preferred_name.first if it exists and is non-empty, otherwise use name.first
first_name_to_use = self.name.first
if (
hasattr(self, "preferred_name")
and hasattr(self.preferred_name, "first")
and self.preferred_name.first
):
first_name_to_use = self.preferred_name.first

first_name_candidates = [person.familiar() for person in unique_names]
if self.name.first not in first_name_candidates:
return self.name.first
if first_name_to_use not in first_name_candidates:
return first_name_to_use

first_name_and_suffix_candidates = [
f"{person.familiar()} {person.name.suffix if hasattr(person.name, 'suffix') else ''}"
for person in unique_names
]
if (
f"{self.name.first} {self.name.suffix if hasattr(self.name, 'suffix') else ''}"
f"{first_name_to_use} {self.name.suffix if hasattr(self.name, 'suffix') else ''}"
not in first_name_and_suffix_candidates
):
if hasattr(self.name, "suffix") and self.name.suffix:
return f"{self.name.first} {self.name.suffix if hasattr(self.name, 'suffix') else ''}"
return self.name.first
return f"{first_name_to_use} {self.name.suffix if hasattr(self.name, 'suffix') else ''}"
return first_name_to_use

first_and_middle_candidates = [
f"{person.name.first} {person.name.middle if hasattr(person.name, 'middle') and person.name.middle else ''}"
for person in unique_names
]
if (
f"{self.name.first} {self.name.middle if hasattr(self.name, 'middle') and self.name.middle else ''}"
f"{first_name_to_use} {self.name.middle if hasattr(self.name, 'middle') and self.name.middle else ''}"
not in first_and_middle_candidates
):
if hasattr(self.name, "middle") and self.name.middle:
return f"{self.name.first} {self.name.middle}"
return self.name.first
return f"{first_name_to_use} {self.name.middle}"
return first_name_to_use

first_and_last_candidates = [
(
Expand Down
132 changes: 132 additions & 0 deletions docassemble/AssemblyLine/data/questions/test_preferred_name.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
include:
- assembly_line.yml
---
metadata:
title: |
Test Preferred Name Functionality
---
mandatory: True
code: |
# Get basic user information
users[0].name.first
users[0].name.last

# Choose test scenario
test_scenario

# Handle different scenarios
if test_scenario == 'with_preferred':
users[0].preferred_name.first
elif test_scenario == 'business_with_preferred':
users[0].person_type = 'business'
users[0].preferred_name.first
elif test_scenario == 'conflict_resolution':
# Create another person to test conflict resolution
other_people.gather()
users[0].preferred_name.first
# For 'without_preferred', we don't ask for preferred name

show_results
---
question: |
Which test scenario would you like to run?
field: test_scenario
choices:
- Test with preferred name: with_preferred
- Test without preferred name: without_preferred
- Test business with preferred name: business_with_preferred
- Test conflict resolution: conflict_resolution
---
question: |
What is your preferred first name?
subquestion: |
This is the name you'd like to be called, which may be different from your legal first name.

Leave blank if you want to use your legal first name.
fields:
- Preferred first name: users[0].preferred_name.first
required: False
---
question: |
What is the other person's name?
subquestion: |
We need another person to test name conflict resolution.
fields:
- First name: other_people[0].name.first
- Last name: other_people[0].name.last
---
objects:
- other_people: ALPeopleList.using(object_type=ALIndividual, complete_attribute="complete")
---
question: |
Is there anyone else?
yesno: other_people.there_is_another
---
code: |
other_people[i].name.first
other_people[i].complete = True
---
event: show_results
question: |
Test Results
subquestion: |
#### Test Scenario: ${ test_scenario }

#### Your Information:

- Legal name: ${ users[0].name.first } ${ users[0].name.last }
% if test_scenario == 'business_with_preferred':
- Person type: ${ users[0].person_type }
% endif
% if test_scenario in ['with_preferred', 'business_with_preferred', 'conflict_resolution']:
- Preferred name: ${ users[0].preferred_name.first if users[0].preferred_name.first else "(not set)" }
% endif

% if test_scenario == 'conflict_resolution' and other_people.number() > 0:
#### Other People:
% for person in other_people:
- ${ person.name.first } ${ person.name.last }
% endfor
% endif

---

#### Familiar Name Results:

% if test_scenario == 'conflict_resolution' and other_people.number() > 0:
- Without considering conflicts: `${ users[0].familiar() }`
- Considering conflicts with other people: `${ users[0].familiar(unique_names=other_people) }`
% else:
- Your familiar name: `${ users[0].familiar() }`
% endif

---

#### What this means:

% if test_scenario == 'with_preferred':
% if users[0].preferred_name.first:
Since you provided a preferred name ("${ users[0].preferred_name.first }"), the `.familiar()` method uses it instead of your legal first name.
% else:
Since you didn't provide a preferred name, the `.familiar()` method uses your legal first name ("${ users[0].name.first }").
% endif
% elif test_scenario == 'without_preferred':
No preferred name was requested, so the `.familiar()` method uses your legal first name ("${ users[0].name.first }").
% elif test_scenario == 'business_with_preferred':
% if users[0].preferred_name.first:
Even though you provided a preferred name ("${ users[0].preferred_name.first }"), because this is a business entity, the `.familiar()` method ignores the preferred name and uses the legal name ("${ users[0].name.first }").
% else:
This is a business entity, so the `.familiar()` method uses the legal name ("${ users[0].name.first }").
% endif
% elif test_scenario == 'conflict_resolution':
% if users[0].preferred_name.first:
With your preferred name "${ users[0].preferred_name.first }":
% else:
With your legal name "${ users[0].name.first }":
% endif
- When no other people are considered, you get: `${ users[0].familiar() }`
- When considering potential name conflicts with other people, the system tries to find a unique way to refer to you: `${ users[0].familiar(unique_names=other_people) }`
% endif
buttons:
- Start over: restart
130 changes: 129 additions & 1 deletion docassemble/AssemblyLine/test_al_general.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import unittest
from .al_general import ALIndividual, ALAddress, get_visible_al_nav_items
from unittest.mock import Mock
from unittest.mock import Mock, patch
from docassemble.base.util import DADict, DAAttributeError
from docassemble.base.functions import value as da_value


class test_aladdress(unittest.TestCase):
Expand Down Expand Up @@ -81,6 +82,72 @@ def setUp(self):

self.individual.name.first = "John"

label_defaults = {
"first_name_label": "First name",
"middle_name_label": "Middle name",
"last_name_label": "Last name",
"suffix_label": "Suffix",
"name_title_label": "Title",
"business_name_label": "Business name",
"person_type_label": "Person type",
"individual_choice_label": "Person",
"business_choice_label": "Business",
"gender_label": "Gender",
"gender_female_label": "Female",
"gender_male_label": "Male",
"gender_nonbinary_label": "Nonbinary",
"gender_prefer_not_to_say_label": "Prefer not to say",
"gender_prefer_self_described_label": "Self described",
"gender_self_described_label": "Self described gender",
"gender_unknown_label": "Unknown",
"gender_help_text": "Help text",
"pronouns_label": "Pronouns",
"pronouns_help_text": "Pronouns help",
"pronoun_prefer_not_to_say_label": "Prefer not to say",
"pronoun_unknown_label": "Unknown",
"pronoun_prefer_self_described_label": "Something else",
"pronoun_self_described_label": "Self described pronouns",
"language_label": "Language",
"language_other_label": "Other language",
}

for attr, value in label_defaults.items():
setattr(self.individual, attr, value)

self._functions_value_patcher = patch(
"docassemble.base.functions.value",
side_effect=self._mock_value_with_defaults,
)
self._util_value_patcher = patch(
"docassemble.base.util.value",
side_effect=self._mock_value_with_defaults,
)
self._al_general_value_patcher = patch(
"docassemble.AssemblyLine.al_general.value",
side_effect=self._mock_value_with_defaults,
)
self._functions_value_patcher.start()
self._util_value_patcher.start()
self._al_general_value_patcher.start()

def tearDown(self):
self._al_general_value_patcher.stop()
self._util_value_patcher.stop()
self._functions_value_patcher.stop()

def _mock_value_with_defaults(self, variable_name, *args, **kwargs):
if variable_name == "al_name_suffixes":
return ["Jr.", "Sr."]
if variable_name == "al_name_titles":
return ["Mr.", "Ms."]
if variable_name == "al_pronoun_choices":
return [
{"He/him/his": "he/him/his"},
{"She/her/hers": "she/her/hers"},
{"They/them/theirs": "they/them/theirs"},
]
return da_value(variable_name, *args, **kwargs)

def test_phone_numbers(self):
self.assertEqual(self.individual.phone_numbers(), "")
self.individual.phone_number = ""
Expand Down Expand Up @@ -575,6 +642,67 @@ def test_language_fields_required(self):
other_field = fields[1]
self.assertEqual(other_field["required"], False)

def test_familiar_with_preferred_name(self):
"""Test familiar() method with preferred_name set."""
# Setup basic name
self.individual.name.first = "John"
self.individual.name.last = "Doe"

# Test without preferred_name
self.assertEqual(self.individual.familiar(), "John")

# Set preferred_name
self.individual.preferred_name.first = "Johnny"

# Test with preferred_name
self.assertEqual(self.individual.familiar(), "Johnny")

# Test with empty preferred_name (should fall back to name.first)
self.individual.preferred_name.first = ""
self.assertEqual(self.individual.familiar(), "John")

# Test with None preferred_name (should fall back to name.first)
self.individual.preferred_name.first = None
self.assertEqual(self.individual.familiar(), "John")

# Test with whitespace-only preferred_name (should fall back to name.first)
self.individual.preferred_name.first = " "
# Note: " " is truthy in Python, so this would use the whitespace
# This is acceptable behavior - only empty string and None should fallback
self.assertEqual(self.individual.familiar(), " ")

def test_familiar_with_preferred_name_business(self):
"""Test that business/organization types still work correctly with preferred_name."""
self.individual.person_type = "business"
self.individual.name.first = "Acme Corp"
self.individual.preferred_name.first = "ACME"

# Business types should ignore preferred_name and use name.first
self.assertEqual(self.individual.familiar(), "Acme Corp")

self.individual.person_type = "organization"
self.assertEqual(self.individual.familiar(), "Acme Corp")

def test_familiar_with_preferred_name_and_conflicts(self):
"""Test familiar() with preferred_name when there are name conflicts."""
# Setup multiple people with potential conflicts
other_person = ALIndividual()
other_person.name.first = "Johnny"
other_person.name.last = "Smith"

self.individual.name.first = "John"
self.individual.name.last = "Doe"
self.individual.preferred_name.first = (
"Johnny" # Conflicts with other person's name.first
)

# When there's a conflict with preferred name, should try next option
result = self.individual.familiar(unique_names=[other_person])
# Since "Johnny" conflicts with other_person.familiar(), it should try other combinations
# The exact result depends on the full logic, but it shouldn't just return "Johnny"
self.assertIsInstance(result, str)
self.assertTrue(len(result) > 0)


class test_get_visible_al_nav_items(unittest.TestCase):
def test_case_1(self):
Expand Down
Loading