Skip to content

Commit 55e719a

Browse files
tw4lSuaYoo
andcommitted
Make changes to collections to support publicly listed collections (#2164)
Fixes #2158 - Adds `Organization.listPublicCollections` field and API endpoint to update it - Replaces `Collection.isPublic` boolean with `Collection.access` (values: `private`, `unlisted`, `public`) and add database migration - Update frontend to use `Collection.access` instead of `isPublic`, otherwise not changing current behavior --------- Co-authored-by: sua yoo <[email protected]>
1 parent 5898196 commit 55e719a

File tree

11 files changed

+224
-28
lines changed

11 files changed

+224
-28
lines changed

backend/btrixcloud/colls.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async def add_collection(self, oid: UUID, coll_in: CollIn):
8989
name=coll_in.name,
9090
description=coll_in.description,
9191
modified=modified,
92-
isPublic=coll_in.isPublic,
92+
access=coll_in.access,
9393
)
9494
try:
9595
await self.collections.insert_one(coll.to_dict())
@@ -189,7 +189,7 @@ async def get_collection(
189189
"""Get collection by id"""
190190
query: dict[str, object] = {"_id": coll_id}
191191
if public_only:
192-
query["isPublic"] = True
192+
query["access"] = {"$in": ["public", "unlisted"]}
193193

194194
result = await self.collections.find_one(query)
195195
if not result:
@@ -210,6 +210,7 @@ async def list_collections(
210210
sort_direction: int = 1,
211211
name: Optional[str] = None,
212212
name_prefix: Optional[str] = None,
213+
access: Optional[str] = None,
213214
):
214215
"""List all collections for org"""
215216
# pylint: disable=too-many-locals, duplicate-code
@@ -226,6 +227,9 @@ async def list_collections(
226227
regex_pattern = f"^{name_prefix}"
227228
match_query["name"] = {"$regex": regex_pattern, "$options": "i"}
228229

230+
if access:
231+
match_query["access"] = access
232+
229233
aggregate = [{"$match": match_query}]
230234

231235
if sort_by:
@@ -427,6 +431,7 @@ async def list_collection_all(
427431
sortDirection: int = 1,
428432
name: Optional[str] = None,
429433
namePrefix: Optional[str] = None,
434+
access: Optional[str] = None,
430435
):
431436
collections, total = await colls.list_collections(
432437
org.id,
@@ -436,6 +441,7 @@ async def list_collection_all(
436441
sort_direction=sortDirection,
437442
name=name,
438443
name_prefix=namePrefix,
444+
access=access,
439445
)
440446
return paginated_format(collections, total, page, pageSize)
441447

backend/btrixcloud/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from .migrations import BaseMigration
1818

1919

20-
CURR_DB_VERSION = "0035"
20+
CURR_DB_VERSION = "0036"
2121

2222

2323
# ============================================================================
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Migration 0036 -- collection access
3+
"""
4+
5+
from btrixcloud.migrations import BaseMigration
6+
7+
8+
MIGRATION_VERSION = "0036"
9+
10+
11+
class Migration(BaseMigration):
12+
"""Migration class."""
13+
14+
# pylint: disable=unused-argument
15+
def __init__(self, mdb, **kwargs):
16+
super().__init__(mdb, migration_version=MIGRATION_VERSION)
17+
18+
async def migrate_up(self):
19+
"""Perform migration up.
20+
21+
Move from Collection.isPublic cool to Collection.access enum
22+
"""
23+
colls_mdb = self.mdb["collections"]
24+
25+
# Set non-public collections to private
26+
try:
27+
await colls_mdb.update_many(
28+
{"isPublic": False},
29+
{"$set": {"access": "private"}, "$unset": {"isPublic": 1}},
30+
)
31+
# pylint: disable=broad-exception-caught
32+
except Exception as err:
33+
print(
34+
f"Error migrating private collections: {err}",
35+
flush=True,
36+
)
37+
38+
# Set public collections to unlisted
39+
try:
40+
await colls_mdb.update_many(
41+
{"isPublic": True},
42+
{"$set": {"access": "unlisted"}, "$unset": {"isPublic": 1}},
43+
)
44+
# pylint: disable=broad-exception-caught
45+
except Exception as err:
46+
print(
47+
f"Error migrating public unlisted collections: {err}",
48+
flush=True,
49+
)

backend/btrixcloud/models.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,15 @@ class UpdateUpload(UpdateCrawl):
10551055
### COLLECTIONS ###
10561056

10571057

1058+
# ============================================================================
1059+
class CollAccessType(str, Enum):
1060+
"""Collection access types"""
1061+
1062+
PRIVATE = "private"
1063+
UNLISTED = "unlisted"
1064+
PUBLIC = "public"
1065+
1066+
10581067
# ============================================================================
10591068
class Collection(BaseMongoModel):
10601069
"""Org collection structure"""
@@ -1071,7 +1080,7 @@ class Collection(BaseMongoModel):
10711080
# Sorted by count, descending
10721081
tags: Optional[List[str]] = []
10731082

1074-
isPublic: Optional[bool] = False
1083+
access: CollAccessType = CollAccessType.PRIVATE
10751084

10761085

10771086
# ============================================================================
@@ -1082,7 +1091,7 @@ class CollIn(BaseModel):
10821091
description: Optional[str] = None
10831092
crawlIds: Optional[List[str]] = []
10841093

1085-
isPublic: bool = False
1094+
access: CollAccessType = CollAccessType.PRIVATE
10861095

10871096

10881097
# ============================================================================
@@ -1098,7 +1107,7 @@ class UpdateColl(BaseModel):
10981107

10991108
name: Optional[str] = None
11001109
description: Optional[str] = None
1101-
isPublic: Optional[bool] = None
1110+
access: Optional[CollAccessType] = None
11021111

11031112

11041113
# ============================================================================
@@ -1371,6 +1380,13 @@ class OrgReadOnlyUpdate(BaseModel):
13711380
readOnlyReason: Optional[str] = None
13721381

13731382

1383+
# ============================================================================
1384+
class OrgListPublicCollectionsUpdate(BaseModel):
1385+
"""Organization listPublicCollections update"""
1386+
1387+
listPublicCollections: bool
1388+
1389+
13741390
# ============================================================================
13751391
class OrgWebhookUrls(BaseModel):
13761392
"""Organization webhook URLs"""
@@ -1439,6 +1455,8 @@ class OrgOut(BaseMongoModel):
14391455
allowedProxies: list[str] = []
14401456
crawlingDefaults: Optional[CrawlConfigDefaults] = None
14411457

1458+
listPublicCollections: bool = False
1459+
14421460

14431461
# ============================================================================
14441462
class Organization(BaseMongoModel):
@@ -1494,6 +1512,8 @@ class Organization(BaseMongoModel):
14941512
allowedProxies: list[str] = []
14951513
crawlingDefaults: Optional[CrawlConfigDefaults] = None
14961514

1515+
listPublicCollections: bool = False
1516+
14971517
def is_owner(self, user):
14981518
"""Check if user is owner"""
14991519
return self._is_auth(user, UserRole.OWNER)

backend/btrixcloud/orgs.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
RemovedResponse,
7979
OrgSlugsResponse,
8080
OrgImportResponse,
81+
OrgListPublicCollectionsUpdate,
8182
)
8283
from .pagination import DEFAULT_PAGE_SIZE, paginated_format
8384
from .utils import (
@@ -940,7 +941,7 @@ async def get_org_metrics(self, org: Organization) -> dict[str, int]:
940941
)
941942
collections_count = await self.colls_db.count_documents({"oid": org.id})
942943
public_collections_count = await self.colls_db.count_documents(
943-
{"oid": org.id, "isPublic": True}
944+
{"oid": org.id, "access": {"$in": ["public", "unlisted"]}}
944945
)
945946

946947
return {
@@ -997,6 +998,16 @@ async def update_read_only_on_cancel(
997998
)
998999
return res is not None
9991000

1001+
async def update_list_public_collections(
1002+
self, org: Organization, list_public_collections: bool
1003+
):
1004+
"""Update listPublicCollections field on organization"""
1005+
res = await self.orgs.find_one_and_update(
1006+
{"_id": org.id},
1007+
{"$set": {"listPublicCollections": list_public_collections}},
1008+
)
1009+
return res is not None
1010+
10001011
async def export_org(
10011012
self, org: Organization, user_manager: UserManager
10021013
) -> StreamingResponse:
@@ -1553,6 +1564,19 @@ async def update_read_only_on_cancel(
15531564

15541565
return {"updated": True}
15551566

1567+
@router.post(
1568+
"/list-public-collections",
1569+
tags=["organizations", "collections"],
1570+
response_model=UpdatedResponse,
1571+
)
1572+
async def update_list_public_collections(
1573+
update: OrgListPublicCollectionsUpdate,
1574+
org: Organization = Depends(org_owner_dep),
1575+
):
1576+
await ops.update_list_public_collections(org, update.listPublicCollections)
1577+
1578+
return {"updated": True}
1579+
15561580
@router.post(
15571581
"/event-webhook-urls", tags=["organizations"], response_model=UpdatedResponse
15581582
)

backend/test/test_collections.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_create_public_collection(
5858
json={
5959
"crawlIds": [crawler_crawl_id],
6060
"name": PUBLIC_COLLECTION_NAME,
61-
"isPublic": True,
61+
"access": "public",
6262
},
6363
)
6464
assert r.status_code == 200
@@ -73,7 +73,7 @@ def test_create_public_collection(
7373
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_public_coll_id}",
7474
headers=crawler_auth_headers,
7575
)
76-
assert r.json()["isPublic"]
76+
assert r.json()["access"] == "public"
7777

7878

7979
def test_create_collection_taken_name(
@@ -311,12 +311,31 @@ def test_collection_public(crawler_auth_headers, default_org_id):
311311
)
312312
assert r.status_code == 404
313313

314-
# make public
314+
# make public and test replay headers
315315
r = requests.patch(
316316
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_coll_id}",
317317
headers=crawler_auth_headers,
318318
json={
319-
"isPublic": True,
319+
"access": "public",
320+
},
321+
)
322+
assert r.status_code == 200
323+
assert r.json()["updated"]
324+
325+
r = requests.get(
326+
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_coll_id}/public/replay.json",
327+
headers=crawler_auth_headers,
328+
)
329+
assert r.status_code == 200
330+
assert r.headers["Access-Control-Allow-Origin"] == "*"
331+
assert r.headers["Access-Control-Allow-Headers"] == "*"
332+
333+
# make unlisted and test replay headers
334+
r = requests.patch(
335+
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_coll_id}",
336+
headers=crawler_auth_headers,
337+
json={
338+
"access": "unlisted",
320339
},
321340
)
322341
assert r.status_code == 200
@@ -335,7 +354,7 @@ def test_collection_public(crawler_auth_headers, default_org_id):
335354
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_coll_id}",
336355
headers=crawler_auth_headers,
337356
json={
338-
"isPublic": False,
357+
"access": "private",
339358
},
340359
)
341360

@@ -346,6 +365,24 @@ def test_collection_public(crawler_auth_headers, default_org_id):
346365
assert r.status_code == 404
347366

348367

368+
def test_collection_access_invalid_value(crawler_auth_headers, default_org_id):
369+
r = requests.patch(
370+
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_coll_id}",
371+
headers=crawler_auth_headers,
372+
json={
373+
"access": "invalid",
374+
},
375+
)
376+
assert r.status_code == 422
377+
378+
r = requests.get(
379+
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_coll_id}",
380+
headers=crawler_auth_headers,
381+
)
382+
assert r.status_code == 200
383+
assert r.json()["access"] == "private"
384+
385+
349386
def test_add_upload_to_collection(crawler_auth_headers, default_org_id):
350387
with open(os.path.join(curr_dir, "data", "example.wacz"), "rb") as fh:
351388
r = requests.put(
@@ -429,6 +466,7 @@ def test_list_collections(
429466
assert first_coll["totalSize"] > 0
430467
assert first_coll["modified"]
431468
assert first_coll["tags"] == ["wr-test-2", "wr-test-1"]
469+
assert first_coll["access"] == "private"
432470

433471
second_coll = [coll for coll in items if coll["name"] == SECOND_COLLECTION_NAME][0]
434472
assert second_coll["id"]
@@ -440,6 +478,7 @@ def test_list_collections(
440478
assert second_coll["totalSize"] > 0
441479
assert second_coll["modified"]
442480
assert second_coll["tags"] == ["wr-test-2"]
481+
assert second_coll["access"] == "private"
443482

444483

445484
def test_remove_upload_from_collection(crawler_auth_headers, default_org_id):
@@ -525,6 +564,26 @@ def test_filter_sort_collections(
525564
assert coll["oid"] == default_org_id
526565
assert coll.get("description") is None
527566

567+
# Test filtering by access
568+
name_prefix = name_prefix.upper()
569+
r = requests.get(
570+
f"{API_PREFIX}/orgs/{default_org_id}/collections?access=public",
571+
headers=crawler_auth_headers,
572+
)
573+
assert r.status_code == 200
574+
data = r.json()
575+
assert data["total"] == 1
576+
577+
items = data["items"]
578+
assert len(items) == 1
579+
580+
coll = items[0]
581+
assert coll["id"]
582+
assert coll["name"] == PUBLIC_COLLECTION_NAME
583+
assert coll["oid"] == default_org_id
584+
assert coll.get("description") is None
585+
assert coll["access"] == "public"
586+
528587
# Test sorting by name, ascending (default)
529588
r = requests.get(
530589
f"{API_PREFIX}/orgs/{default_org_id}/collections?sortBy=name",

backend/test/test_org.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,24 @@ def test_update_read_only(admin_auth_headers, default_org_id):
697697
assert data["readOnlyReason"] == ""
698698

699699

700+
def test_update_list_public_collections(admin_auth_headers, default_org_id):
701+
# Test that default is false
702+
r = requests.get(f"{API_PREFIX}/orgs/{default_org_id}", headers=admin_auth_headers)
703+
assert r.json()["listPublicCollections"] is False
704+
705+
# Update
706+
r = requests.post(
707+
f"{API_PREFIX}/orgs/{default_org_id}/list-public-collections",
708+
headers=admin_auth_headers,
709+
json={"listPublicCollections": True},
710+
)
711+
assert r.json()["updated"]
712+
713+
# Test update is reflected in GET response
714+
r = requests.get(f"{API_PREFIX}/orgs/{default_org_id}", headers=admin_auth_headers)
715+
assert r.json()["listPublicCollections"]
716+
717+
700718
def test_sort_orgs(admin_auth_headers):
701719
# Create a few new orgs for testing
702720
r = requests.post(

0 commit comments

Comments
 (0)