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, role: UserRole = UserRole.VIEWER,
page_size: int = DEFAULT_PAGE_SIZE, page_size: int = DEFAULT_PAGE_SIZE,
page: int = 1, page: int = 1,
sort_by: Optional[str] = "name",
sort_direction: int = 1,
calculate_total=True, calculate_total=True,
): ):
"""Get all orgs a user is a member of""" """Get all orgs a user is a member of"""
@ -142,19 +144,52 @@ class OrgOps:
skip = page_size * page skip = page_size * page
if user.is_superuser: if user.is_superuser:
query = {} query: Dict[str, object] = {}
else: else:
query = {f"users.{user.id}": {"$gte": role.value}} query: Dict[str, object] = {f"users.{user.id}": {"$gte": role.value}}
total = 0 aggregate = [{"$match": query}]
if calculate_total:
total = await self.orgs.count_documents(query)
cursor = self.orgs.find(query, skip=skip, limit=page_size) # Ensure default org is always first, then sort on sort_by if set
results = await cursor.to_list(length=page_size) sort_query = {"default": -1}
orgs = [Organization.from_dict(res) for res in results]
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( async def get_org_for_user_by_id(
self, oid: UUID, user: User, role: UserRole = UserRole.VIEWER 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), user: User = Depends(user_dep),
pageSize: int = DEFAULT_PAGE_SIZE, pageSize: int = DEFAULT_PAGE_SIZE,
page: int = 1, page: int = 1,
sortBy: Optional[str] = "name",
sortDirection: Optional[int] = 1,
): ):
results, total = await ops.get_orgs_for_user( 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 = [ serialized_results = [
await res.serialize_for_user(user, user_manager) for res in 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, "giftedExecMinutes": 0,
} }
assert "subData" not in org 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 { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import { type PropertyValues, type TemplateResult } from "lit"; import { type PropertyValues, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import sortBy from "lodash/fp/sortBy";
import type { InviteSuccessDetail } from "@/features/accounts/invite-form"; import type { InviteSuccessDetail } from "@/features/accounts/invite-form";
import type { APIPaginatedList } from "@/types/api"; import type { APIPaginatedList } from "@/types/api";
@ -278,12 +277,12 @@ export class Home extends LiteElement {
} }
private async fetchOrgs() { private async fetchOrgs() {
this.orgList = sortBy<OrgData>("name")(await this.getOrgs()); this.orgList = await this.getOrgs();
} }
private async getOrgs() { private async getOrgs() {
const data = await this.apiFetch<APIPaginatedList<OrgData>>( const data = await this.apiFetch<APIPaginatedList<OrgData>>(
"/orgs", "/orgs?sortBy=name",
this.authState!, this.authState!,
); );