- 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.
118 lines
5.2 KiB
Markdown
118 lines
5.2 KiB
Markdown
# 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](exports.md) 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](errors.md#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:
|
|
|
|
```json
|
|
{"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](tokens-and-api-keys.md#scopes)).
|
|
|
|
## The object
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{"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. A `cf_<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:
|
|
|
|
```json
|
|
{"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:
|
|
|
|
```json
|
|
{"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.
|