feat(scholars): add multiselect and bulk actions (delete, toggle, export)
Add Set<number>-based selection state to ScholarsPage with checkboxes in both table and card views, select-all toggle, and stale-key pruning. Backend: POST /scholars/bulk-delete, POST /scholars/bulk-toggle, and GET /scholars/export?ids= filter. Service functions use user-scoped queries. Structured logging for bulk operations. Frontend: useScholarBulkActions composable extracts selection and bulk action logic. AppSelect-based action dropdown with dynamic options. Delete prompts window.confirm; enable/disable applies immediately. Export downloads filtered JSON. Includes integration tests for all bulk endpoints and frontend unit tests for selection toggle, select-all, and stale pruning.
This commit is contained in:
parent
6fa54db08f
commit
7f78dd6353
9 changed files with 730 additions and 79 deletions
|
|
@ -56,11 +56,14 @@ async def export_user_data(
|
|||
db_session: AsyncSession,
|
||||
*,
|
||||
user_id: int,
|
||||
scholar_profile_ids: list[int] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
scholars_result = await db_session.execute(
|
||||
select(ScholarProfile).where(ScholarProfile.user_id == user_id).order_by(ScholarProfile.id.asc())
|
||||
)
|
||||
publication_result = await db_session.execute(
|
||||
scholar_query = select(ScholarProfile).where(ScholarProfile.user_id == user_id)
|
||||
if scholar_profile_ids:
|
||||
scholar_query = scholar_query.where(ScholarProfile.id.in_(scholar_profile_ids))
|
||||
scholars_result = await db_session.execute(scholar_query.order_by(ScholarProfile.id.asc()))
|
||||
|
||||
pub_query = (
|
||||
select(
|
||||
ScholarProfile.scholar_id,
|
||||
Publication.cluster_id,
|
||||
|
|
@ -77,8 +80,13 @@ async def export_user_data(
|
|||
.join(ScholarPublication, ScholarPublication.scholar_profile_id == ScholarProfile.id)
|
||||
.join(Publication, Publication.id == ScholarPublication.publication_id)
|
||||
.where(ScholarProfile.user_id == user_id)
|
||||
.order_by(ScholarPublication.created_at.desc(), Publication.id.desc())
|
||||
)
|
||||
if scholar_profile_ids:
|
||||
pub_query = pub_query.where(ScholarProfile.id.in_(scholar_profile_ids))
|
||||
publication_result = await db_session.execute(
|
||||
pub_query.order_by(ScholarPublication.created_at.desc(), Publication.id.desc())
|
||||
)
|
||||
|
||||
scholars = [_serialize_export_scholar(profile) for profile in scholars_result.scalars().all()]
|
||||
publications = [_serialize_export_publication(row) for row in publication_result.all()]
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -27,10 +27,61 @@ from app.services.scholars.validators import (
|
|||
validate_scholar_id,
|
||||
)
|
||||
|
||||
async def bulk_delete_scholars(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
user_id: int,
|
||||
scholar_profile_ids: list[int],
|
||||
upload_dir: str | None = None,
|
||||
) -> int:
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db_session.execute(
|
||||
select(ScholarProfile).where(
|
||||
ScholarProfile.id.in_(scholar_profile_ids),
|
||||
ScholarProfile.user_id == user_id,
|
||||
)
|
||||
)
|
||||
profiles = list(result.scalars().all())
|
||||
if not profiles:
|
||||
return 0
|
||||
if upload_dir:
|
||||
upload_root = _ensure_upload_root(upload_dir, create=True)
|
||||
for profile in profiles:
|
||||
_safe_remove_upload(upload_root, profile.profile_image_upload_path)
|
||||
for profile in profiles:
|
||||
await db_session.delete(profile)
|
||||
await db_session.commit()
|
||||
return len(profiles)
|
||||
|
||||
|
||||
async def bulk_toggle_scholars(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
user_id: int,
|
||||
scholar_profile_ids: list[int],
|
||||
is_enabled: bool,
|
||||
) -> int:
|
||||
from sqlalchemy import update
|
||||
|
||||
result = await db_session.execute(
|
||||
update(ScholarProfile)
|
||||
.where(
|
||||
ScholarProfile.id.in_(scholar_profile_ids),
|
||||
ScholarProfile.user_id == user_id,
|
||||
)
|
||||
.values(is_enabled=is_enabled)
|
||||
)
|
||||
await db_session.commit()
|
||||
return result.rowcount # type: ignore[return-value]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SEARCH_COOLDOWN_REASON",
|
||||
"SEARCH_DISABLED_REASON",
|
||||
"ScholarServiceError",
|
||||
"bulk_delete_scholars",
|
||||
"bulk_toggle_scholars",
|
||||
"clear_profile_image_customization",
|
||||
"create_scholar_for_user",
|
||||
"delete_scholar",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue