Skip to content

Add owner: operator to advanced search#2821

Open
bendichter wants to merge 5 commits into
dandi:masterfrom
bendichter:advanced-search-owner
Open

Add owner: operator to advanced search#2821
bendichter wants to merge 5 commits into
dandi:masterfrom
bendichter:advanced-search-owner

Conversation

@bendichter
Copy link
Copy Markdown
Member

@bendichter bendichter commented May 8, 2026

Adds an owner: operator to the advanced-search syntax (which landed in #2814).

owner:VALUE returns dandisets owned by users matching VALUE. The lookup is case-insensitive against User.username, User.email, User.first_name, User.last_name, or "first_name last_name" (so the display name shown in the dandiset list also works). Multiple users may match (common with last-name lookups); we union dandisets owned by any of them. Unknown user → 0 results, not 400.

owner:alice
owner:alice@example.com
owner:Smith              # any user named Smith
owner:"Jane Doe"         # full display name

The unquoted owner:me magic alias for "the current user" was discussed and deferred to a follow-up PR. Reasons in the thread on the contributor PR. For now, owner:me is treated as a literal lookup for users named "Me" (matches none in practice).

Implementation notes
  • _apply_owner_filter in dandiapi/api/services/search/filters.py does the lookup. Direct query against DandisetUserObjectPermission (rather than guardian's get_objects_for_user) so we can union across multiple matched users in a single query and bypass the superuser-sees-everything default — owner:admin returns only what admin explicitly owns, not the entire archive.
  • OpenAPI help text on DandisetQueryParameterSerializer.search lists the operator alongside the others.
  • Frontend popover entry: owner:"Jane Doe".
Test plan
  • tox -e lint
  • tox -e test -- dandiapi/api/tests/test_dandiset.py -k "advanced_search_owner" — three consolidated tests cover username/email/case-insensitivity/full-name/multi-user-union/unknown-user/superuser-non-inflation/cross-key combination.
  • Manual: curl 'http://localhost:8000/api/dandisets/?search=owner:%22Jane%20Doe%22'.

@bendichter bendichter changed the title Add owner: operator to advanced search (stacked on #2814) Add owner: operator to advanced search May 8, 2026
@bendichter bendichter requested a review from yarikoptic May 8, 2026 13:12
Copy link
Copy Markdown
Member

@yarikoptic yarikoptic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial HI review. Overall -- love it.

Comment thread dandiapi/api/tests/test_dandiset.py Outdated
alice_ds = DandisetFactory.create(owners=[alice])
DraftVersionFactory.create(dandiset=alice_ds)

assert _search_ids(api_client, 'owner:alice@example.com') == {alice_ds.identifier}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please ask your agent to remember to make "each test matter more" kinda... I will try to figure out where/how I did it to mine later -- e.g. these two tests could be 1, and likely others could also be folded in where more testing done on the same "setup" thus making overall testing faster and without loss of any coverage. I do understand that there is a tradeoff (individual aspect tests might be easier to debug etc) but still -- my Allergy to duplication is triggering AIlergy too

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I generally prefer more modular tests, but yes I can adjust for DANDI projects

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8a65f3b — consolidated the 10 owner tests down to 3 denser ones (lookup-paths-and-combinations, me-magic-and-literal-escape, superuser-non-inflation). Same coverage; DB setup runs ~3x instead of ~10x. Saved a memory so my agent prefers the denser style for DANDI PRs going forward.

Comment thread dandiapi/api/tests/test_dandiset.py Outdated
# though their username is an email address.
user = UserFactory.create(
username='ben.dichter@gmail.com',
email='ben.dichter@gmail.com',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might better to use some alice.m.nobody@example.com from above instead of leaking personal real emails

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha good point

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8a65f3b — switched the fixture user to a generic Super/User name, no personal email.

Comment thread dandiapi/api/services/search/filters.py Outdated
`get_objects_for_user` so we can intersect across multiple matched users
in a single query, and to bypass the superuser-sees-everything default.
"""
if value.lower() == 'me':
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now I started to "feel" for the unlikely Me Someoneyou being unable to look himself without authorization... I wonder if we could make it more explicit, e.g. owner:%me% or owner:me: or alike -- any other system like github or google having smth like that? could also be owner-me: keyword . This should make it explicit and it is better than implicit

Copy link
Copy Markdown
Member Author

@bendichter bendichter May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

owner:"me" should search for the name "Me". I can test that. Also, if you wanted to search for an owner named "Me Someoneyou", you could use owner:"Me Someoneyou".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8a65f3b. Addressed via a parser-level quoted/unquoted distinction:

  • owner:me (unquoted) → magic alias for the requesting user (anonymous → 400 with a hint pointing at the quoted form)
  • owner:"me" (quoted) → bypasses the magic and matches the literal user named Me
  • owner:"Me Someoneyou" → matches the full-name display

Implementation: ParsedSearch.operators is now a list of Operator dataclasses (key, value, quoted) instead of bare 2-tuples; only the owner filter inspects quoted for now, but the same signal could be used by other operators that ever want a magic-alias escape hatch. New integration test test_advanced_search_owner_me_magic_and_literal_escape pins all three cases plus the anonymous-400 path.

GitHub uses @me for the same idea, but that's because @ is GitHub's user sigil — we don't have that convention in DANDI, so the @ would just be an arbitrary literal here. The quoted-form escape feels more in line with the Gmail-style operator syntax we already have (which uses "foo:bar" as the colon escape).

bendichter added a commit to bendichter/dandi-archive that referenced this pull request May 8, 2026
Round-2 review feedback on dandi#2821:

- @yarikoptic flagged that owner:me silently shadows a real user named "Me".
  Fix: distinguish quoted vs unquoted at the parser level. Unquoted
  owner:me → magic alias for the requesting user. Quoted owner:"me" →
  literal lookup (matches a user whose first/last name is "Me"). Same
  pattern lets owner:"Me Someoneyou" reach the literal full-name match
  while keeping the convenient owner:me shortcut.

  Implementation: ParsedSearch.operators is now a list of `Operator`
  dataclasses (key, value, quoted) instead of bare tuples. Filters
  consume the new shape and the owner filter switches on the quoted
  flag.

- Replaced personal email (ben.dichter@gmail.com) in the full-name test
  fixture with a generic example user.

- Consolidated 10 small owner-tests into 3 denser ones that share setup
  per @yarikoptic's "make each test matter more" feedback. Coverage is
  unchanged (every documented lookup path is asserted; cross-key AND
  with another operator; multi-user union via shared last name; unknown
  user → 0; superuser non-inflation; owner:me magic; owner:"me"
  literal-escape; anonymous owner:me → 400). DB setup runs ~3x instead
  of ~10x.

Updated OpenAPI help text and the search popover to mention the
owner:me alias and the quoted-escape.
@bendichter bendichter requested a review from yarikoptic May 8, 2026 16:41
Filters dandisets to those owned by a given user. The value is matched
case-insensitively against User.username OR User.email. The special form
`owner:me` resolves to the requesting user (consistent with the existing
?user=me query parameter) and returns 400 if the request is anonymous.

Implementation reuses the existing `get_owned_dandisets()` permission
helper. We pass `with_superuser=False` so `owner:admin` returns only
what admin explicitly owns — guardian's default would otherwise inflate
to the entire archive for any superuser.

Unknown users return zero results (not an error): a search for a
nonexistent owner is a valid 0-hit query.

Tests cover username/email lookup, case-insensitivity, unknown user,
`owner:me` for an authenticated user, anonymous `owner:me` → 400, the
superuser non-inflation guarantee, and combination with other operators.

OpenAPI help text and the frontend operator popover updated.
Real users encounter the dandiset list with owners shown by display
name (e.g. "Super User"), not by username. Searching that string was
returning 0 because the lookup only matched username/email.

Now matches case-insensitively against username, email, first_name,
last_name, OR "first_name last_name" — so owner:"Super User" works
the same as owner:ben.dichter@gmail.com.

Multiple users may match (e.g. shared last name); we union dandisets
owned by any of them via a direct DandisetUserObjectPermission query.
Updated OpenAPI help text and the frontend popover example to
`owner:"Jane Doe"` so users discover the new shape.
Round-2 review feedback on dandi#2821:

- @yarikoptic flagged that owner:me silently shadows a real user named "Me".
  Fix: distinguish quoted vs unquoted at the parser level. Unquoted
  owner:me → magic alias for the requesting user. Quoted owner:"me" →
  literal lookup (matches a user whose first/last name is "Me"). Same
  pattern lets owner:"Me Someoneyou" reach the literal full-name match
  while keeping the convenient owner:me shortcut.

  Implementation: ParsedSearch.operators is now a list of `Operator`
  dataclasses (key, value, quoted) instead of bare tuples. Filters
  consume the new shape and the owner filter switches on the quoted
  flag.

- Replaced personal email (ben.dichter@gmail.com) in the full-name test
  fixture with a generic example user.

- Consolidated 10 small owner-tests into 3 denser ones that share setup
  per @yarikoptic's "make each test matter more" feedback. Coverage is
  unchanged (every documented lookup path is asserted; cross-key AND
  with another operator; multi-user union via shared last name; unknown
  user → 0; superuser non-inflation; owner:me magic; owner:"me"
  literal-escape; anonymous owner:me → 400). DB setup runs ~3x instead
  of ~10x.

Updated OpenAPI help text and the search popover to mention the
owner:me alias and the quoted-escape.
The unquoted owner:me → current-user shortcut required threading a
`quoted` flag through the parser and a `request_user` arg through the
filter dispatch — non-trivial machinery to support one alias.

Per dandi#2822 review discussion, removing it from this PR keeps the owner
operator focused on literal lookup-by-value (username / email / first /
last / "first last") and avoids the design debate about the right escape
mechanism for "I literally want a user named Me." The alias can come
back in a focused follow-up PR if/when there's appetite for it.

Concrete drops:
- owner:me magic + 400-on-anonymous in `_apply_owner_filter`
- `Operator.quoted` field on the parser dataclass
- `quoted` and `request_user` parameters on `_apply_owner_filter`
- `get_owned_dandisets` import (no longer used here)
- `test_advanced_search_owner_me_magic_and_literal_escape` test
- The two `owner-me-quoted` / `owner-me-unquoted` parser test cases
- "owner:me" mentions in OpenAPI help text and the popover entry
@bendichter bendichter force-pushed the advanced-search-owner branch from 30535f9 to a328da1 Compare May 13, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants