refactor: Improve person creation and identification logic with optional fields handling

This commit refactors the person creation and identification logic to handle optional fields more effectively. The `date_of_birth` field in the `PersonCreateRequest` schema is now optional, and the frontend has been updated to trim whitespace from name fields before submission. Additionally, the identification logic has been enhanced to ensure that only non-empty names are considered valid. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09 2025-11-27 13:02:02 -05:00
parent 709be7555a
commit 999e79f859
7 changed files with 88 additions and 54 deletions

View File

@ -28,7 +28,7 @@ export interface PersonCreateRequest {
last_name: string
middle_name?: string
maiden_name?: string
date_of_birth: string
date_of_birth?: string | null
}
export interface PersonUpdateRequest {

View File

@ -82,8 +82,10 @@ export default function Identify() {
const restorationCompleteRef = useRef(false)
const canIdentify = useMemo(() => {
return Boolean((personId && currentFace) || (firstName && lastName && dob && currentFace))
}, [personId, firstName, lastName, dob, currentFace])
if (!currentFace) return false
if (personId) return true
return Boolean(firstName.trim() && lastName.trim())
}, [personId, firstName, lastName, currentFace])
const loadFaces = async (clearState: boolean = false, ignorePhotoIds: boolean = false) => {
setLoadingFaces(true)
@ -589,6 +591,11 @@ export default function Identify() {
const handleIdentify = async () => {
if (!currentFace) return
setBusy(true)
const trimmedFirstName = firstName.trim()
const trimmedLastName = lastName.trim()
const trimmedMiddleName = middleName.trim()
const trimmedMaidenName = maidenName.trim()
const trimmedDob = dob.trim()
const additional = Object.entries(selectedSimilar)
.filter(([, v]) => v)
.map(([k]) => Number(k))
@ -597,11 +604,17 @@ export default function Identify() {
if (personId) {
payload.person_id = personId
} else {
payload.first_name = firstName
payload.last_name = lastName
payload.middle_name = middleName || undefined
payload.maiden_name = maidenName || undefined
payload.date_of_birth = dob
payload.first_name = trimmedFirstName
payload.last_name = trimmedLastName
if (trimmedMiddleName) {
payload.middle_name = trimmedMiddleName
}
if (trimmedMaidenName) {
payload.maiden_name = trimmedMaidenName
}
if (trimmedDob) {
payload.date_of_birth = trimmedDob
}
}
await facesApi.identify(currentFace.id, payload)
// Optimistic: remove identified faces from list
@ -948,7 +961,7 @@ export default function Identify() {
disabled={!!personId} />
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">Date of Birth *</label>
<label className="block text-sm font-medium text-gray-700">Date of Birth (optional)</label>
<input type="date" value={dob} onChange={(e) => {
setDob(e.target.value)
setPersonId(undefined) // Clear person selection when typing

View File

@ -11,17 +11,39 @@ from sqlalchemy import inspect
from src.web.db.session import engine, get_database_url
from src.web.db.models import Base
# Ordered list ensures foreign-key dependents drop first
TARGET_TABLES = [
"photo_favorites",
"phototaglinkage",
"person_encodings",
"faces",
"tags",
"photos",
"people",
]
def drop_all_tables():
"""Drop all tables from the database."""
db_url = get_database_url()
print(f"Connecting to database: {db_url}")
# Drop all tables
print("\nDropping all tables...")
Base.metadata.drop_all(bind=engine)
inspector = inspect(engine)
existing_tables = set(inspector.get_table_names())
print("✅ All tables dropped successfully!")
print("\nDropping selected tables...")
for table_name in TARGET_TABLES:
if table_name not in Base.metadata.tables:
print(f" ⚠️ Table '{table_name}' not found in metadata, skipping.")
continue
if table_name not in existing_tables:
print(f" Table '{table_name}' does not exist in database, skipping.")
continue
table = Base.metadata.tables[table_name]
print(f" 🗑️ Dropping '{table_name}'...")
table.drop(bind=engine, checkfirst=True)
print("✅ Selected tables dropped successfully!")
print("\nYou can now recreate tables using:")
print(" python scripts/recreate_tables_web.py")

View File

@ -290,16 +290,20 @@ def identify_face(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="person_id not found")
else:
# Validate required fields for creation
if not (request.first_name and request.last_name and request.date_of_birth):
first_name = (request.first_name or "").strip()
last_name = (request.last_name or "").strip()
middle_name = request.middle_name.strip() if request.middle_name else None
maiden_name = request.maiden_name.strip() if request.maiden_name else None
if not (first_name and last_name):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="first_name, last_name and date_of_birth are required to create a person",
detail="first_name and last_name are required to create a person",
)
person = Person(
first_name=request.first_name,
last_name=request.last_name,
middle_name=request.middle_name,
maiden_name=request.maiden_name,
first_name=first_name,
last_name=last_name,
middle_name=middle_name,
maiden_name=maiden_name,
date_of_birth=request.date_of_birth,
)
db.add(person)

View File

@ -304,41 +304,32 @@ def approve_deny_pending_identifications(
# Check if person already exists (by name and DOB)
# Match the unique constraint: first_name, last_name, middle_name, maiden_name, date_of_birth
person = None
# Build query with proper None handling
query = main_db.query(Person).filter(
Person.first_name == row.first_name,
Person.last_name == row.last_name,
)
# Handle optional fields - use IS NULL for None values
if row.middle_name:
query = query.filter(Person.middle_name == row.middle_name)
else:
query = query.filter(Person.middle_name.is_(None))
if row.maiden_name:
query = query.filter(Person.maiden_name == row.maiden_name)
else:
query = query.filter(Person.maiden_name.is_(None))
if row.date_of_birth:
# Build query with proper None handling
query = main_db.query(Person).filter(
Person.first_name == row.first_name,
Person.last_name == row.last_name,
Person.date_of_birth == row.date_of_birth
)
# Handle optional fields - use IS NULL for None values
if row.middle_name:
query = query.filter(Person.middle_name == row.middle_name)
else:
query = query.filter(Person.middle_name.is_(None))
if row.maiden_name:
query = query.filter(Person.maiden_name == row.maiden_name)
else:
query = query.filter(Person.maiden_name.is_(None))
person = query.first()
query = query.filter(Person.date_of_birth == row.date_of_birth)
else:
query = query.filter(Person.date_of_birth.is_(None))
person = query.first()
# Create person if doesn't exist
created_person = False
if not person:
if not row.date_of_birth:
errors.append(f"Pending identification {decision.id} missing date_of_birth (required for person creation)")
auth_db.execute(text("""
UPDATE pending_identifications
SET status = 'denied', updated_at = :updated_at
WHERE id = :id
"""), {"id": decision.id, "updated_at": datetime.utcnow()})
auth_db.commit()
denied_count += 1
continue
person = Person(
first_name=row.first_name,
last_name=row.last_name,

View File

@ -95,11 +95,15 @@ def list_people_with_faces(
@router.post("", response_model=PersonResponse, status_code=status.HTTP_201_CREATED)
def create_person(request: PersonCreateRequest, db: Session = Depends(get_db)) -> PersonResponse:
"""Create a new person."""
first_name = request.first_name.strip()
last_name = request.last_name.strip()
middle_name = request.middle_name.strip() if request.middle_name else None
maiden_name = request.maiden_name.strip() if request.maiden_name else None
person = Person(
first_name=request.first_name,
last_name=request.last_name,
middle_name=request.middle_name,
maiden_name=request.maiden_name,
first_name=first_name,
last_name=last_name,
middle_name=middle_name,
maiden_name=maiden_name,
date_of_birth=request.date_of_birth,
)
db.add(person)

View File

@ -30,7 +30,7 @@ class PersonCreateRequest(BaseModel):
last_name: str = Field(..., min_length=1)
middle_name: Optional[str] = None
maiden_name: Optional[str] = None
date_of_birth: date
date_of_birth: Optional[date] = None
class PeopleListResponse(BaseModel):