Add and apply backend sorting for org list

The default org will always be sorted first, regardless of sort options.
Orgs after the first will be sorted by name ascending by default.
Sorting currently supported on name, slug, and readOnly.
This commit is contained in:
Tessa Walsh 2024-07-03 11:01:01 -04:00
parent ed0d489cda
commit d3fb33a78a
3 changed files with 110 additions and 13 deletions

View File

@ -134,6 +134,8 @@ class OrgOps:
role: UserRole = UserRole.VIEWER,
page_size: int = DEFAULT_PAGE_SIZE,
page: int = 1,
sort_by: Optional[str] = "name",
sort_direction: int = 1,
calculate_total=True,
):
"""Get all orgs a user is a member of"""
@ -142,19 +144,52 @@ class OrgOps:
skip = page_size * page
if user.is_superuser:
query = {}
query: Dict[str, object] = {}
else:
query = {f"users.{user.id}": {"$gte": role.value}}
query: Dict[str, object] = {f"users.{user.id}": {"$gte": role.value}}
total = 0
if calculate_total:
total = await self.orgs.count_documents(query)
aggregate = [{"$match": query}]
cursor = self.orgs.find(query, skip=skip, limit=page_size)
results = await cursor.to_list(length=page_size)
orgs = [Organization.from_dict(res) for res in results]
# Ensure default org is always first, then sort on sort_by if set
sort_query = {"default": -1}
return orgs, total
if sort_by:
sort_fields = ("name", "slug", "readOnly")
if sort_by not in sort_fields:
raise HTTPException(status_code=400, detail="invalid_sort_by")
if sort_direction not in (1, -1):
raise HTTPException(status_code=400, detail="invalid_sort_direction")
sort_query[sort_by] = sort_direction
aggregate.extend([{"$sort": sort_query}])
aggregate.extend(
[
{
"$facet": {
"items": [
{"$skip": skip},
{"$limit": page_size},
],
"total": [{"$count": "count"}],
}
},
]
)
# Get total
cursor = self.orgs.aggregate(aggregate)
results = await cursor.to_list(length=1)
result = results[0]
items = result["items"]
try:
total = int(result["total"][0]["count"])
except (IndexError, ValueError):
total = 0
return [Organization.from_dict(data) for data in items], total
async def get_org_for_user_by_id(
self, oid: UUID, user: User, role: UserRole = UserRole.VIEWER
@ -788,9 +823,15 @@ def init_orgs_api(app, mdb, user_manager, invites, user_dep, user_or_shared_secr
user: User = Depends(user_dep),
pageSize: int = DEFAULT_PAGE_SIZE,
page: int = 1,
sortBy: Optional[str] = "name",
sortDirection: Optional[int] = 1,
):
results, total = await ops.get_orgs_for_user(
user, page_size=pageSize, page=page
user,
page_size=pageSize,
page=page,
sort_by=sortBy,
sort_direction=sortDirection,
)
serialized_results = [
await res.serialize_for_user(user, user_manager) for res in results

View File

@ -703,3 +703,60 @@ def test_create_org_and_invite_existing_user(admin_auth_headers):
"giftedExecMinutes": 0,
}
assert "subData" not in org
def test_sort_orgs(admin_auth_headers):
# Create a few new orgs for testing
r = requests.post(
f"{API_PREFIX}/orgs/create",
headers=admin_auth_headers,
json={"name": "abc", "slug": "abc"},
)
assert r.status_code == 200
r = requests.post(
f"{API_PREFIX}/orgs/create",
headers=admin_auth_headers,
json={"name": "mno", "slug": "mno"},
)
assert r.status_code == 200
r = requests.post(
f"{API_PREFIX}/orgs/create",
headers=admin_auth_headers,
json={"name": "xyz", "slug": "xyz"},
)
assert r.status_code == 200
# Check default sorting
# Default org should come first, followed by alphabetical sorting ascending
r = requests.get(f"{API_PREFIX}/orgs", headers=admin_auth_headers)
data = r.json()
orgs = data["items"]
assert orgs[0]["default"]
other_orgs = orgs[1:]
last_name = None
for org in other_orgs:
org_name = org["name"]
if last_name:
assert org_name > last_name
last_name = org_name
# Sort by name descending, ensure default org still first
r = requests.get(
f"{API_PREFIX}/orgs?sortBy=name&sortDirection=-1", headers=admin_auth_headers
)
data = r.json()
orgs = data["items"]
assert orgs[0]["default"]
other_orgs = orgs[1:]
last_name = None
for org in other_orgs:
org_name = org["name"]
if last_name:
assert org_name < last_name
last_name = org_name

View File

@ -2,7 +2,6 @@ import { localized, msg, str } from "@lit/localize";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import { type PropertyValues, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import sortBy from "lodash/fp/sortBy";
import type { InviteSuccessDetail } from "@/features/accounts/invite-form";
import type { APIPaginatedList } from "@/types/api";
@ -278,12 +277,12 @@ export class Home extends LiteElement {
}
private async fetchOrgs() {
this.orgList = sortBy<OrgData>("name")(await this.getOrgs());
this.orgList = await this.getOrgs();
}
private async getOrgs() {
const data = await this.apiFetch<APIPaginatedList<OrgData>>(
"/orgs",
"/orgs?sortBy=name",
this.authState!,
);