Ask
26

How do I get a dropdown into a form whose options depend on the current user or another field?

I have a form with a dropdown that should only offer options belonging to the logged-in user's organisation. At the moment it offers every option in the table, which is both wrong and a data leak.

Defining the field on the form class works fine for a static list, but I need the options decided per request, and the form class does not know who is asking.

I have seen people build the field in the view and assign it, which works but feels wrong, and I have seen people filter it in the template, which definitely feels wrong.

What is the correct place for this?

4 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @upgrade_notes_ute · 4h ago

    Add a test for the rejection case, not just the display case.

    The test everyone writes is "user A sees only their own options". The test that matters is "user A submits user B's option id directly and the form rejects it". Those are different code paths and only the second one is a security test.

    It is three lines and it is the one that fails when somebody later moves the filtering into the view or the template for convenience.

    Same thinking for the ordering: if a field is required, test that submitting it empty fails, because narrowing an option source to an empty set turns a required field into an impossible one, and that is a bug users report as "the page is broken".

    26
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @form_field_farid · 22h ago

    The correct place is the form's initialiser, and the pattern is standard enough that it is worth learning once and reusing forever.

    The shape:

    1. The form's initialiser accepts an extra keyword argument — the user, the organisation, whatever the options depend on.
    2. It removes that argument before calling the parent initialiser, because the parent does not know about it and will reject it.
    3. It calls the parent.
    4. It then narrows the field's option source using the value it kept.

    The view passes the extra argument when it constructs the form. That is the whole pattern.

    Why the initialiser and not the class body. The class body runs once, when the module is imported — at startup, in a process that will serve thousands of requests for different users. Anything you evaluate there is shared across all of them. That is exactly why a class-level definition cannot be user-specific, and it is also the source of a related bug people hit: putting a call to "today's date" or a database query at class level and finding it frozen at process start.

    The initialiser runs per form instance, which is per request. That is the right scope.

    Why not in the view. You can assign the field's option source from the view and it works. But then the constraint lives outside the form, so any other view using that form silently has no constraint — and this is a security constraint. Keeping it in the form means the form is safe wherever it is used, and there is one place to audit.

    Why definitely not in the template. Filtering the displayed options in the template changes only what is displayed. The form still accepts any value in the full set, so anyone can submit an option they were not shown and it validates. That is the data leak you were worried about, still fully present, now invisible.

    This is the most important point here: narrowing the choices is a validation change, not a presentation change. The form must reject values outside the allowed set, and it only does that if the field's option source itself is narrowed.

    A related case worth handling at the same time: a dropdown whose options depend on another field in the same form — pick a country, then a city. That cannot be done server-side in one render, because the first field's value is not known until the user chooses. The options are:

    • Two requests. The page reloads or fetches the dependent options when the first field changes. Simple and robust.
    • Send everything and filter client-side. Fine for small sets, and it leaks the whole set to the client, so never for anything access-controlled.

    Either way, validate the combination on the server on submit. Whatever the browser was shown is not a constraint.

    30
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @csrf_carla · 2d ago

    For the initialiser pattern specifically, be careful about how you accept the extra argument.

    If you take it positionally you will break every existing call site and the errors will be confusing. Take it as a keyword argument with a default, and remove it before delegating upward. That way the form still works when nobody passes it — degrading to whatever default you choose — and existing code keeps working.

    Decide deliberately what the default is, though. A default that means "no filtering" is a security footgun waiting for someone to forget the argument. A default that means "nothing" is safer and fails visibly.

    21
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @token_shape_theo · 18m ago

    If the same constraint applies across many forms — everything scoped to the user's organisation — it is worth pushing it down to the query layer rather than repeating the initialiser pattern in twenty forms.

    A manager or a scoped query that is always organisation-filtered means the form's option source is correct by construction, and a developer who forgets the pattern gets a safe default instead of a leak.

    More work up front. Much better than relying on everyone remembering.

    1
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report