Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions server/polar/backoffice/organizations/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,8 @@ def get_value(self, item: File) -> str | None:
async def get(
request: Request,
id: UUID4,
files_page: int = Query(1, ge=1),
files_limit: int = Query(10, ge=1, le=100),
session: AsyncSession = Depends(get_db_session),
) -> Any:
repository = OrganizationRepository.from_session(session)
Expand Down Expand Up @@ -1803,7 +1805,7 @@ async def get(
pass

# Organization Files Section
with tag.div(classes="mt-8"):
with tag.div(classes="mt-8 flex flex-col gap-4", id="files"):
with tag.div(classes="flex items-center gap-4 mb-4"):
with tag.h2(classes="text-2xl font-bold"):
text("Downloadable Files")
Expand All @@ -1812,10 +1814,12 @@ async def get(
(FileSortProperty.created_at, True)
]
file_repository = FileRepository.from_session(session)
files = await file_repository.get_all_by_organization(
files, files_count = await file_repository.paginate_by_organization(
organization.id,
service=FileServiceTypes.downloadable,
sorting=sorting,
limit=files_limit,
page=files_page,
)

with datatable.Datatable[File, FileSortProperty](
Expand All @@ -1828,6 +1832,11 @@ async def get(
).render(request, files, sorting=sorting):
pass

with datatable.pagination(
request, PaginationParams(files_page, files_limit), files_count
):
pass


@router.get("/{id}/plain_search_url", name="organizations:plain_search_url")
async def get_plain_search_url(
Expand Down
16 changes: 13 additions & 3 deletions server/polar/backoffice/organizations_v2/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ async def get_organization_detail(
request: Request,
organization_id: UUID4,
section: str = Query("overview"),
files_page: int = Query(1, ge=1),
files_limit: int = Query(10, ge=1, le=100),
session: AsyncSession = Depends(get_db_session),
) -> None:
"""
Expand Down Expand Up @@ -498,17 +500,25 @@ async def get_organization_detail(
with account_section.render(request):
pass
elif section == "files":
# Fetch downloadable files from repository
# Fetch downloadable files from repository with pagination
file_sorting: list[Sorting[FileSortProperty]] = [
(FileSortProperty.created_at, True)
]
file_repository = FileRepository(session)
files = await file_repository.get_all_by_organization(
files, files_count = await file_repository.paginate_by_organization(
organization.id,
service=FileServiceTypes.downloadable,
sorting=file_sorting,
limit=files_limit,
page=files_page,
)
files_section = FilesSection(
organization,
files=files,
page=files_page,
limit=files_limit,
total_count=files_count,
)
files_section = FilesSection(organization, files=list(files))
with files_section.render(request):
pass
elif section == "history":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from collections.abc import Generator

from fastapi import Request
from tagflow import tag, text
from starlette.datastructures import URL
from tagflow import attr, tag, text

from polar.file.service import file as file_service
from polar.models import File, Organization
Expand All @@ -19,9 +20,15 @@ def __init__(
self,
organization: Organization,
files: list[File] | None = None,
page: int = 1,
limit: int = 10,
total_count: int = 0,
):
self.org = organization
self.files = files or []
self.page = page
self.limit = limit
self.total_count = total_count

def format_file_size(self, size_bytes: int) -> str:
"""Format file size in human-readable format."""
Expand All @@ -34,18 +41,63 @@ def format_file_size(self, size_bytes: int) -> str:
else:
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"

def _render_pagination(self, request: Request) -> None:
"""Render pagination controls for files."""
start = (self.page - 1) * self.limit + 1
end = min(self.page * self.limit, self.total_count)

# Calculate URLs for navigation
next_url: URL | None = None
if end < self.total_count:
next_url = request.url.replace_query_params(
**{**request.query_params, "files_page": self.page + 1}
).replace(fragment="files")
previous_url: URL | None = None
if start > 1:
previous_url = request.url.replace_query_params(
**{**request.query_params, "files_page": self.page - 1}
).replace(fragment="files")

with tag.div(classes="flex justify-between"):
with tag.div(classes="text-sm"):
text("Showing ")
with tag.span(classes="font-bold"):
text(str(start))
text(" to ")
with tag.span(classes="font-bold"):
text(str(end))
text(" of ")
with tag.span(classes="font-bold"):
text(str(self.total_count))
text(" entries")
with tag.div(classes="join grid grid-cols-2"):
with tag.a(
classes="join-item btn",
href=str(previous_url) if previous_url else "",
):
if previous_url is None:
attr("disabled", True)
text("Previous")
with tag.a(
classes="join-item btn",
href=str(next_url) if next_url else "",
):
if next_url is None:
attr("disabled", True)
text("Next")

@contextlib.contextmanager
def render(self, request: Request) -> Generator[None]:
"""Render the files section."""

with tag.div(classes="space-y-6"):
with tag.div(classes="space-y-6", id="files"):
# Files list card
with card(bordered=True):
with tag.div(classes="flex items-center justify-between mb-4"):
with tag.h2(classes="text-lg font-bold"):
text("Downloadable Files")
with tag.div(classes="text-sm text-base-content/60"):
text(f"{len(self.files)} file(s)")
text(f"{self.total_count} file(s)")

if self.files:
# Files table
Expand Down Expand Up @@ -104,6 +156,10 @@ def render(self, request: Request) -> Generator[None]:
classes="btn btn-sm btn-ghost",
):
text("Download")

# Pagination controls
if self.total_count > self.limit:
self._render_pagination(request)
else:
with empty_state(
"No Files",
Expand All @@ -117,19 +173,19 @@ def render(self, request: Request) -> Generator[None]:
text("Storage Information")

with tag.div(classes="space-y-2 text-sm"):
total_size = sum(f.size for f in self.files if f.size)
page_size = sum(f.size for f in self.files if f.size)

with tag.div(classes="flex justify-between"):
with tag.span(classes="text-base-content/60"):
text("Total Files:")
with tag.span(classes="font-semibold"):
text(str(len(self.files)))
text(str(self.total_count))

with tag.div(classes="flex justify-between"):
with tag.span(classes="text-base-content/60"):
text("Total Size:")
text("Page Size:")
with tag.span(classes="font-semibold"):
text(self.format_file_size(total_size))
text(self.format_file_size(page_size))

yield

Expand Down
24 changes: 24 additions & 0 deletions server/polar/file/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,30 @@ async def get_all_by_organization(

return await self.get_all(statement)

async def paginate_by_organization(
self,
organization_id: UUID,
*,
service: FileServiceTypes | None = None,
sorting: list[Sorting[FileSortProperty]] = [
(FileSortProperty.created_at, True)
],
limit: int,
page: int,
) -> tuple[list[File], int]:
"""Get paginated files for an organization, optionally filtered by service type."""
statement = self.get_base_statement().where(
File.organization_id == organization_id,
File.is_uploaded.is_(True),
)

if service is not None:
statement = statement.where(File.service == service)

statement = self.apply_sorting(statement, sorting)

return await self.paginate(statement, limit=limit, page=page)

def get_sorting_clause(self, property: FileSortProperty) -> SortingClause:
match property:
case FileSortProperty.created_at:
Expand Down
Loading