Nobody has started this yet — be first.
Business impact
Today, importing a CSV of contacts that references any group name not already created in the system fails every row that mentions it with "unknown group(s): [...]". In practice this means a user migrating from another contact-management tool (a very common use case for a CSV import feature) has to first manually pre-create every single group name that appears anywhere in their file -- a tedious, error-prone, entirely avoidable extra step for what's supposed to be a one-click bulk import. This is exactly the kind of friction that makes people give up on a migration and open a support ticket asking someone to "just import this for me."
Problem
import_contacts() resolves each row's groups column (semicolon-separated group names) by looking each one up with Group.query.filter_by(name=name). Any name that doesn't already exist is collected into unknown_groups and the whole row is failed with a ValidationError. There is no way to opt into a different behavior -- the request body/form is never consulted for any kind of flag.
Current behavior
A CSV import of rows referencing a brand-new group name (e.g. "Newbies") fails every one of those rows with "unknown group(s): ['Newbies']", even when the caller passed create_missing_groups=true.
Expected behavior
When the import request includes create_missing_groups=true (as a form field alongside the uploaded file), a group name that doesn't exist yet should be created instead of failing the row. If the same new group name appears on multiple rows within one import batch, it must be created only once -- every later row referencing that name should reuse the group that was just created, not attempt to create it again. Without the flag, behavior is unchanged: unknown group names still fail the row exactly as they do today.
Steps to reproduce
printf 'name,phone,email,groups\nNew Person One,555-1000,one@example.com,Newbies\nNew Person Two,555-2000,two@example.com,Newbies\n' > /tmp/contacts.csv
curl -s -X POST http://127.0.0.1:5000/api/contacts/import
-F "file=@/tmp/contacts.csv;type=text/csv"
-F "create_missing_groups=true"
Why this matters
This is a "check-then-act" problem shaped like the setup for a real race condition, even in this single-request, single-threaded context: row 1 checks "does 'Newbies' exist?" (no), so it must create it -- but by the time row 2 asks the same question a few lines later in the same loop, the correct answer has changed because row 1 already created it in this same transaction/session. Get the bookkeeping wrong (e.g. always re-querying the database instead of remembering what you already created this batch, or creating first and querying second) and you either violate the unique constraint on Group.name or end up creating the same group twice.
Suggested approach
Look at the group-name resolution loop inside import_contacts() (app/blueprints/contacts/routes.py, around where unknown_groups is built up). You'll need to: read the new flag from request.form; decide what happens to a name in unknown_groups when the flag is set; and make sure a name created for one row is visible to (and reused by) every subsequent row in the same import -- think about what in-memory bookkeeping, scoped to a single import_contacts() call, would let you avoid a second database round-trip for a group you already know you just created.
Acceptance criteria
Verification
SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_07_csv_auto_create_groups.py -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 feat/csv-import-auto-create-groupsFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/csv-import-auto-create-groupsSubmit 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 2
Implement a feature
Extend the system within its own patterns.