Skip to content

Commit 9e92817

Browse files
authored
Merge pull request #23 from VectorInstitute/add_admin_status_report_command
Add cli command to view onboarding status of participants
2 parents 153324f + 8775d7f commit 9e92817

File tree

4 files changed

+353
-4
lines changed

4 files changed

+353
-4
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "aieng-platform-onboard"
3-
version = "0.3.2"
3+
version = "0.3.3"
44
description = "CLI tool for onboarding participants to AI Engineering bootcamps"
55
readme = "README.md"
66
authors = [{name = "Vector AI Engineering", email = "[email protected]"}]
@@ -16,6 +16,7 @@ dependencies = [
1616
"requests>=2.31.0",
1717
"python-dotenv>=1.0.0",
1818
"rich>=13.0.0",
19+
"pandas>=2.3.3",
1920
]
2021

2122
[project.scripts]

src/aieng_platform_onboard/cli.py

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@
1515
from typing import Any
1616

1717
from rich.panel import Panel
18+
from rich.table import Table
1819

1920
from aieng_platform_onboard.utils import (
2021
check_onboarded_status,
2122
console,
2223
create_env_file,
2324
fetch_token_from_service,
25+
get_all_participants_with_status,
2426
get_github_user,
2527
get_global_keys,
2628
get_participant_data,
2729
get_team_data,
30+
initialize_firestore_admin,
2831
initialize_firestore_with_token,
2932
update_onboarded_status,
3033
validate_env_file,
@@ -258,6 +261,121 @@ def _setup_environment(
258261
return output_path
259262

260263

264+
def display_onboarding_status_report(gcp_project: str) -> int:
265+
"""
266+
Display onboarding status report for all participants.
267+
268+
This function is for admin use only and requires proper GCP service
269+
account credentials. It fetches all participants from Firestore and
270+
displays their onboarding status in a table.
271+
272+
Parameters
273+
----------
274+
gcp_project : str
275+
GCP project ID.
276+
277+
Returns
278+
-------
279+
int
280+
Exit code (0 for success, 1 for failure).
281+
"""
282+
console.print(
283+
Panel.fit(
284+
"[bold cyan]Onboarding Status Report[/bold cyan]\n"
285+
"Admin view of all participant onboarding status",
286+
border_style="cyan",
287+
)
288+
)
289+
290+
# Initialize Firestore with admin credentials
291+
console.print("\n[cyan]Connecting to Firestore with admin credentials...[/cyan]")
292+
try:
293+
db = initialize_firestore_admin(project_id=gcp_project)
294+
console.print("[green]✓ Connected to Firestore[/green]\n")
295+
except Exception as e:
296+
console.print(
297+
f"[red]✗ Failed to connect to Firestore:[/red]\n"
298+
f" {e}\n\n"
299+
"[yellow]This command requires admin (service account) credentials.[/yellow]\n"
300+
"[dim]Ensure you are authenticated with proper GCP permissions:[/dim]\n"
301+
" gcloud auth application-default login\n"
302+
" [dim]or have GOOGLE_APPLICATION_CREDENTIALS set[/dim]"
303+
)
304+
return 1
305+
306+
# Fetch all participants
307+
console.print("[cyan]Fetching participant data...[/cyan]")
308+
try:
309+
participants = get_all_participants_with_status(db)
310+
console.print(f"[green]✓ Found {len(participants)} participants[/green]\n")
311+
except Exception as e:
312+
console.print(f"[red]✗ Failed to fetch participant data:[/red] {e}")
313+
return 1
314+
315+
if not participants:
316+
console.print(
317+
Panel.fit(
318+
"[yellow]No participants found in Firestore[/yellow]\n\n"
319+
"[dim]Use admin scripts to add participants first[/dim]",
320+
border_style="yellow",
321+
)
322+
)
323+
return 0
324+
325+
# Create and display status table
326+
table = Table(
327+
title="Participant Onboarding Status",
328+
show_header=True,
329+
header_style="bold cyan",
330+
show_lines=True,
331+
)
332+
table.add_column("GitHub Handle", style="yellow", no_wrap=True)
333+
table.add_column("Team Name", style="blue")
334+
table.add_column("Status", justify="center")
335+
336+
# Count onboarded vs total
337+
onboarded_count = 0
338+
339+
for participant in participants:
340+
github_handle = participant["github_handle"]
341+
team_name = participant["team_name"]
342+
is_onboarded = participant["onboarded"]
343+
344+
if is_onboarded:
345+
onboarded_count += 1
346+
status = "[green]✓ Onboarded[/green]"
347+
else:
348+
status = "[red]✗ Not Onboarded[/red]"
349+
350+
table.add_row(github_handle, team_name, status)
351+
352+
console.print(table)
353+
console.print()
354+
355+
# Display summary
356+
total_count = len(participants)
357+
not_onboarded_count = total_count - onboarded_count
358+
percentage = (onboarded_count / total_count * 100) if total_count > 0 else 0
359+
360+
summary_text = (
361+
f"[bold]Onboarding Summary[/bold]\n\n"
362+
f"Total Participants: [cyan]{total_count}[/cyan]\n"
363+
f"Onboarded: [green]{onboarded_count}[/green]\n"
364+
f"Not Onboarded: [red]{not_onboarded_count}[/red]\n"
365+
f"Completion Rate: [yellow]{percentage:.1f}%[/yellow]"
366+
)
367+
368+
console.print(
369+
Panel.fit(
370+
summary_text,
371+
border_style="cyan",
372+
title="Summary",
373+
)
374+
)
375+
376+
return 0
377+
378+
261379
def _run_tests_and_finalize(
262380
db: Any, github_user: str, skip_test: bool, test_script: str
263381
) -> bool:
@@ -340,7 +458,6 @@ def main() -> int: # noqa: PLR0911
340458
parser.add_argument(
341459
"--bootcamp-name",
342460
type=str,
343-
required=True,
344461
help="Name of the bootcamp (e.g., fall-2025)",
345462
)
346463
parser.add_argument(
@@ -363,7 +480,6 @@ def main() -> int: # noqa: PLR0911
363480
parser.add_argument(
364481
"--test-script",
365482
type=str,
366-
required=True,
367483
help="Path to integration test script",
368484
)
369485
parser.add_argument(
@@ -376,9 +492,24 @@ def main() -> int: # noqa: PLR0911
376492
action="store_true",
377493
help="Force re-onboarding even if already onboarded",
378494
)
495+
parser.add_argument(
496+
"--admin-status-report",
497+
action="store_true",
498+
help="Display onboarding status for all participants (admin only, requires service account credentials)",
499+
)
379500

380501
args = parser.parse_args()
381502

503+
# Handle admin status report (early return)
504+
if args.admin_status_report:
505+
return display_onboarding_status_report(args.gcp_project)
506+
507+
# Validate required arguments for normal onboarding flow
508+
if not args.bootcamp_name:
509+
parser.error("--bootcamp-name is required for participant onboarding")
510+
if not args.test_script:
511+
parser.error("--test-script is required for participant onboarding")
512+
382513
# Print header
383514
console.print(
384515
Panel.fit(

src/aieng_platform_onboard/utils.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,90 @@ def update_onboarded_status(
557557

558558
except Exception as e:
559559
return False, str(e)
560+
561+
562+
def initialize_firestore_admin(
563+
project_id: str = FIRESTORE_PROJECT_ID,
564+
database_id: str = FIRESTORE_DATABASE_ID,
565+
) -> firestore.Client:
566+
"""
567+
Initialize Firestore client with admin (service account) credentials.
568+
569+
This function uses default Google Cloud credentials, typically a service
570+
account, to connect to Firestore with full admin access. This bypasses
571+
security rules and should only be used by authorized administrators.
572+
573+
Parameters
574+
----------
575+
project_id : str, optional
576+
GCP project ID, by default FIRESTORE_PROJECT_ID.
577+
database_id : str, optional
578+
Firestore database ID, by default FIRESTORE_DATABASE_ID.
579+
580+
Returns
581+
-------
582+
firestore.Client
583+
Authenticated Firestore client with admin access.
584+
585+
Raises
586+
------
587+
Exception
588+
If initialization fails or credentials are not available.
589+
"""
590+
try:
591+
return firestore.Client(project=project_id, database=database_id)
592+
except Exception as e:
593+
raise Exception(
594+
f"Failed to initialize Firestore admin client: {e}\n"
595+
"Ensure you have proper GCP service account credentials configured."
596+
) from e
597+
598+
599+
def get_all_participants_with_status(db: firestore.Client) -> list[dict[str, Any]]:
600+
"""
601+
Retrieve all participants with their onboarding status.
602+
603+
This function fetches all participant documents from Firestore and
604+
returns them with their GitHub handle, team name, and onboarding status.
605+
606+
Parameters
607+
----------
608+
db : firestore.Client
609+
Firestore client instance with admin access.
610+
611+
Returns
612+
-------
613+
list[dict[str, Any]]
614+
List of participant data dictionaries containing:
615+
- github_handle: GitHub username
616+
- team_name: Assigned team name
617+
- onboarded: Boolean onboarding status
618+
- onboarded_at: Timestamp of onboarding (if onboarded)
619+
620+
Raises
621+
------
622+
Exception
623+
If fetching participant data fails.
624+
"""
625+
try:
626+
participants_ref = db.collection("participants")
627+
participants = []
628+
629+
for doc in participants_ref.stream():
630+
participant_data = doc.to_dict()
631+
if participant_data:
632+
participants.append(
633+
{
634+
"github_handle": doc.id,
635+
"team_name": participant_data.get("team_name", "N/A"),
636+
"onboarded": participant_data.get("onboarded", False),
637+
"onboarded_at": participant_data.get("onboarded_at"),
638+
}
639+
)
640+
641+
# Sort by team name, then by github handle
642+
participants.sort(key=lambda x: (x["team_name"], x["github_handle"]))
643+
return participants
644+
645+
except Exception as e:
646+
raise Exception(f"Failed to fetch participant data: {e}") from e

0 commit comments

Comments
 (0)