Nobody has started this yet — be first.
Business impact
Declining an offer is one of the most common, completely normal endings to a job search -- a candidate accepts a different role, negotiates and walks away, or just changes their mind. Right now, the tracker has no legal way to record that: once an application reaches offer, it is permanently stuck there. Every user who reaches this point -- arguably the most important milestone the whole app exists to track -- hits a dead end exactly when they're closest to being done, and their application history is permanently wrong: it'll say "offer" forever even after they've told everyone they turned it down.
Problem
ALLOWED_TRANSITIONS maps STATUS_OFFER to an empty set, so no outgoing transition from offer is considered legal -- including the very common offer -> rejected.
Current behavior
Application.apply_transition(Application.STATUS_REJECTED), called on an application currently at offer, raises ValueError("Cannot transition from 'offer' to 'rejected'.") instead of succeeding.
Expected behavior
PATCH /api/applications/{id}/status/ with {"status": "rejected"} on an application currently at offer succeeds (200), updates status to rejected, and records a StatusChange from offer to rejected -- the same way every other legal transition already does.
Steps to reproduce
python manage.py shell -c " from django.contrib.auth import get_user_model from applications.models import Application u = get_user_model().objects.create_user(username='t4demo', password='pw-t4demo-123') app = Application.objects.create(user=u, company='Acme', role='Engineer') app.apply_transition(Application.STATUS_INTERVIEW) app.apply_transition(Application.STATUS_OFFER) app.apply_transition(Application.STATUS_REJECTED) "
Why this matters
ALLOWED_TRANSITIONS is the single source of truth both the model (Application.apply_transition) and the API (StatusTransitionSerializer.validate_status) consult, so this one-line gap blocks the transition everywhere at once, with a clean, "working as coded" 400 rather than a crash. That's exactly why it's easy to miss: nothing errors, nothing looks broken in isolation, it's just a business rule that was encoded with an empty set where a real target status belongs.
Suggested approach
Look at the shape of the other three entries in ALLOWED_TRANSITIONS -- each of applied and interview already allows transitioning to rejected -- and decide what offer's entry should look like by the same pattern. rejected itself intentionally stays terminal (an empty set); don't change that one.
Acceptance criteria
Verification
python manage.py test practicetickets.ticket04_offer_cannot_be_rejected -v 2
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/offer-cannot-be-rejectedFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/offer-cannot-be-rejectedSubmit 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 in.envhttp://127.0.0.1:8000/api/http://127.0.0.1:8000/admin/To run against Postgres via Docker instead:
docker compose up --build # postgres + web, migrations run automatically on startup
Work the tickets in practicetickets/ (TICKET_01 through TICKET_07); each names one dedicated test, addressed by its explicit dotted label since these modules are deliberately named so Django's default test*.py discovery never picks them up:
python manage.py test practicetickets.ticket01_login_credential_swap -v 2 # a single ticket
./practicetickets/run_tickets.sh # all 7, clean pass/fail summary
Two of the seven bugs (tickets 01 and 06) are real enough to also break the project's own pre-existing suite (python manage.py test) -- fixing them correctly makes those pass again too, with zero changes needed inside accounts/tests.py or applications/tests.py.
Level 1
Fix a bug
Read existing behaviour, correct it.