- Implement tests for `isOrgAdminProvider` to validate admin roles based on organization membership and role. - Create tests for `saved_views_provider` to ensure correct API interactions for saving, renaming, and deleting saved views. - Enhance ticket tree tests to accurately reflect inactive and merged ticket handling. - Update CSV import tests to verify error handling and file saving functionality. - Introduce tests for deal cards to ensure correct display of next steps and task statuses.
5.2 KiB
Saved views
A saved view is a name and a set of list filters, stored for one person so they can put a filtered
list back with one tap. The six lists that export to CSV take one: leads, contacts,
accounts, deals, tickets and invoices. Routes are in backend/common/urls.py, views in
common/views/saved_view_views.py, and what a view may hold in common/saved_views.py.
| Endpoint | Does |
|---|---|
GET /api/saved-views/ |
The caller's own views; ?module=<list> for one list's |
POST /api/saved-views/ |
Save one |
GET /api/saved-views/{id}/ |
One of the caller's views |
PATCH /api/saved-views/{id}/ |
Rename it, or replace its filters |
DELETE /api/saved-views/{id}/ |
Delete it (204) |
PUT is not offered; a view has two things to change and PATCH changes either.
Private to the person who saved it
Every lookup filters on the caller's org and the caller's own profile, admins included: an admin
cannot list or open a member's views. Another person's id answers 404 on every verb, with the
same body as an id that does not exist, so ids cannot be probed (see
Errors: Not found versus forbidden). The table is also
under row-level security keyed on the org.
org and profile are set by the server. A body that names either is refused with a 400, rather
than having the key quietly dropped:
{"profile": ["This is set by the server."]}
For personal access tokens the resource is saved-views: saved-views:read to list and open,
saved-views:write to save, rename and delete (see
Tokens and API keys).
The object
{
"id": "5b0c5c1e-6a0e-4f43-9d6e-1f0f3c1a2b3c",
"module": "cases",
"name": "Urgent, mine",
"filters": {
"status": ["New", "Assigned", "Pending"],
"priority": ["Urgent"],
"assigned_to": ["8a4e...-profile-id"]
},
"created_at": "2026-09-26T10:04:11.201Z",
"updated_at": "2026-09-26T10:04:11.201Z"
}
module is the API name of the list: leads, contacts, accounts, opportunities (deals),
cases (tickets) or invoices. It is fixed once the view exists; a PATCH naming another list is
a 400.
filters holds the list's own query parameters, each as a list of values, so a view is applied by
sending them to the list as they are: the example above is
GET /api/cases/?status=New&status=Assigned&status=Pending&priority=Urgent&assigned_to=..., and
the same query string to GET /api/cases/export/ downloads it. A value sent as one string is stored
as a list of one, and blank values are dropped.
The list response carries the cap alongside the views:
{"saved_views": [ { "...": "the object above" } ], "limit": 25}
What a view may hold
A view is checked before it is stored, so a bad one is a 400 now rather than a list that breaks
every time the view is applied:
- Only the parameters its list reads. Each list's accepted set is written out in
LISTS(common/saved_views.py), beside the function that list builds its queryset with, and a test reads those functions to prove the two agree. Acf_<key>custom-field filter is accepted on all six. Paging (limit,offset) and anything else is refused:{"filters": ["This list does not filter by: limit."]}. - Values the list can parse. The filters are run through the list's own parameter parsing
(the queryset is built, never evaluated), so a malformed id, date or number is refused with the
list's own message:
{"filters": ["assigned_to: 'x' is not a valid id."]}. A value that parses but matches nothing, a status that does not exist for example, is stored, exactly as the list would accept it. - Bounded. At most 30 parameters, 50 values per parameter, and 200 characters per value. Every value must be text.
The ids a view holds are filter values, not references the server resolves: applying a view sends them back to the list, which applies its own read rule, so a view cannot show anyone a record the list would not.
Names and the cap
A name is required, trimmed, and at most 100 characters. Names are unique per person and list,
ignoring case, so Hot leads and hot leads cannot both be saved for leads; a clash is a 400,
also when two saves race each other past the check:
{"name": ["You already have a view with this name for this list."]}
The same name may be used on another list, or by another person.
A person may keep at most 25 views per list (MAX_VIEWS_PER_LIST). Saving one more is a 400;
renaming and changing the filters of an existing view still work at the cap:
{"non_field_errors": ["You can save at most 25 views for one list."]}
In the apps
The web lists and the mobile app both offer a saved-views menu on these six lists, over this API,
so a view saved on one opens on the other. A view means what the list endpoint means: on tickets,
no status is every status, so both clients save the open queue as its three statuses and open a
view without a status as All. Each client saves only the filters its own screen can show and set;
when a view holds a filter the screen has no control for, or several values where the screen takes
one, it still opens and the menu says how many values it left out.