Nobody has started this yet — be first.
Business impact
Search is one of the headline features in the README (?search= matches "substring on name/email/phone"). As shipped right now, a search only returns a contact if the search term appears in all three fields (name, email, and phone) at once. Since most contacts don't have a phone number, or have one that obviously doesn't contain letters from someone's name, this means search returns nothing for almost every real query. Any user relying on search to find a contact -- the single most common thing a contact-book user does -- gets empty results and assumes the person isn't in their contact list at all. This is a silent, severe regression: no error is raised, the API just returns [].
Problem
GET /api/contacts?search=<term> filters contacts using and_(name ilike, email ilike, phone ilike) -- the term must appear in all three columns simultaneously. A contact matching only by name (the overwhelmingly common case) is excluded.
Current behavior
GET /api/contacts?search=Ada returns [] even though a contact named "Ada Lovelace" exists, because her email and phone don't also happen to contain "Ada".
Expected behavior
GET /api/contacts?search=<term> should return every contact where the term appears as a substring in any of name, email, or phone (an OR, not an AND) -- matching the documented behavior in README.md ("substring match on name/email/phone").
Steps to reproduce
curl -s -X POST http://127.0.0.1:5000/api/contacts
-H 'Content-Type: application/json'
-d '{"name":"Ada Lovelace","email":"algorithm.pioneer@example.com"}'
curl -s "http://127.0.0.1:5000/api/contacts?search=Ada"
Why this matters
This is a classic operator-flip mistake: or_(...) accidentally became and_(...), most likely during a refactor or a copy-paste from a different filter that legitimately needed AND semantics. It is a one-word diff that inverts the meaning of the entire filter and is easy to miss in review because the code still "reads fine" -- and_(a, b, c) is syntactically identical in shape to or_(a, b, c).
Suggested approach
Look at the search handling inside list_contacts() in app/blueprints/contacts/routes.py. Think about what combining condition turns "match if ANY field contains the term" into SQL -- and compare that against which SQLAlchemy combinator is actually imported and used there.
Acceptance criteria
Verification
SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_02_search_operator_flip.py -v && SECRET_KEY=dev-secret pytest tests/test_contacts.py -k Search -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/search-and-instead-of-orFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/search-and-instead-of-orSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inBy default DevelopmentConfig uses a local SQLite file, no Postgres server required -- point DATABASE_URL at a real Postgres instance instead if you want one, or use Docker Compose instead (cp .env.example .env then docker compose up --build, which starts a healthchecked Postgres 16 container plus the app served by gunicorn behind entrypoint.sh, which waits for Postgres, runs flask db upgrade, then starts gunicorn -- migrations are always applied on boot). The API is then available at http://localhost:5000 either way.
The existing suite runs under TestingConfig (an in-memory SQLite database created fresh per test, so it never touches your dev database):
source .venv/bin/activate
export SECRET_KEY=dev-secret
pytest -v
Work the tickets in practice_tickets/tickets/ (TICKET-01 through TICKET-07); each names one dedicated test in practice_tickets/tests/ -- a separate pytest package from the project's normal tests/, which plain pytest runs by default:
SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_01_name_length_boundary.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 7, clean pass/fail summary
All 7 dedicated tests fail out of the box -- that is the starting point, not a setup mistake. Fixing TICKET-02 (search operator flip) and TICKET-04 (CSV row-number off-by-one) correctly also turns 4 currently-failing tests in the project's own tests/ suite back to green.
Level 1
Fix a bug
Read existing behaviour, correct it.