gabriel / musehub public
_nav_ctx.py python
136 lines 4.8 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """Shared repo-resolution helper used by all MuseHub UI route modules.
2
3 Each UI page that includes ``repo_nav.html`` / ``repo_tabs.html`` requires:
4 - repo_key, repo_bpm, repo_tags, repo_visibility (displayed in the nav chips)
5 - nav_open_proposal_count, nav_open_issue_count (tab count badges)
6 - nav_domain_viewer_type (controls conditional tabs)
7
8 Centralising this lookup here avoids repeating it in every separate UI route
9 file and keeps the nav_ctx contract in one place.
10 """
11
12 from fastapi import HTTPException
13 from fastapi import status as http_status
14 from sqlalchemy import func, select as sa_select
15 from sqlalchemy.ext.asyncio import AsyncSession
16
17 from musehub.db import musehub_models as musehub_db
18 from musehub.db import musehub_domain_models as domain_db
19 from musehub.services import musehub_repository
20 from musehub.muse_contracts.json_types import JSONObject
21
22
23 async def _resolve_domain_viewer_type(db: AsyncSession, domain_id: str | None) -> str:
24 """Return the viewer_type string for a domain_id, or '' if none registered."""
25 if not domain_id:
26 return ""
27 vtype = await db.scalar(
28 sa_select(domain_db.MusehubDomain.viewer_type)
29 .where(domain_db.MusehubDomain.domain_id == domain_id)
30 )
31 return vtype or ""
32
33
34 async def build_repo_nav_ctx(
35 db: AsyncSession,
36 repo: musehub_db.MusehubRepo,
37 owner: str,
38 repo_slug: str,
39 ) -> JSONObject:
40 """Build the nav_ctx dict from an already-resolved repo ORM object.
41
42 Callers that have already fetched the repo (to avoid a second DB round-trip)
43 should use this instead of ``resolve_repo_with_nav``.
44 """
45 repo_id = str(repo.repo_id)
46
47 proposal_count = await db.scalar(
48 sa_select(func.count())
49 .select_from(musehub_db.MusehubProposal)
50 .where(
51 musehub_db.MusehubProposal.repo_id == repo_id,
52 musehub_db.MusehubProposal.state == "open",
53 )
54 ) or 0
55
56 issue_count = await db.scalar(
57 sa_select(func.count())
58 .select_from(musehub_db.MusehubIssue)
59 .where(
60 musehub_db.MusehubIssue.repo_id == repo_id,
61 musehub_db.MusehubIssue.state == "open",
62 )
63 ) or 0
64
65 domain_viewer_type = await _resolve_domain_viewer_type(
66 db, getattr(repo, "domain_id", None)
67 )
68
69 return {
70 "repo_tags": getattr(repo, "tags", None) or [],
71 "repo_visibility": getattr(repo, "visibility", None) or "private",
72 "nav_open_proposal_count": proposal_count,
73 "nav_open_issue_count": issue_count,
74 "nav_domain_viewer_type": domain_viewer_type,
75 }
76
77
78 async def resolve_repo_with_nav(
79 owner: str,
80 repo_slug: str,
81 db: AsyncSession,
82 ) -> tuple[str, str, JSONObject]:
83 """Resolve owner+slug → (repo_id, base_url, nav_ctx); raise 404 if not found.
84
85 ``nav_ctx`` contains all variables required by ``repo_nav.html`` and
86 ``repo_tabs.html``:
87
88 - ``repo_key`` — key signature (e.g. "C major") or ""
89 - ``repo_bpm`` — tempo in BPM or None
90 - ``repo_tags`` — list of tag strings (may be empty)
91 - ``repo_visibility`` — "public" | "private" | "unlisted"
92 - ``nav_open_proposal_count`` — count of open merge proposals
93 - ``nav_open_issue_count`` — count of open issues
94 - ``nav_domain_viewer_type`` — domain viewer_type string ("code", "") used
95 by ``repo_tabs.html`` to show/hide domain-specific tabs
96
97 Callers should merge nav_ctx into their template context with
98 ``ctx.update(nav_ctx)`` before passing ``ctx`` to ``TemplateResponse`` or
99 ``negotiate_response``.
100 """
101 row = await musehub_repository.get_repo_orm_by_owner_slug(db, owner, repo_slug)
102 if row is None:
103 raise HTTPException(
104 status_code=http_status.HTTP_404_NOT_FOUND,
105 detail=f"Repo '{owner}/{repo_slug}' not found",
106 )
107 repo_id = str(row.repo_id)
108
109 proposal_count = await db.scalar(
110 sa_select(func.count())
111 .select_from(musehub_db.MusehubProposal)
112 .where(
113 musehub_db.MusehubProposal.repo_id == repo_id,
114 musehub_db.MusehubProposal.state == "open",
115 )
116 ) or 0
117
118 issue_count = await db.scalar(
119 sa_select(func.count())
120 .select_from(musehub_db.MusehubIssue)
121 .where(
122 musehub_db.MusehubIssue.repo_id == repo_id,
123 musehub_db.MusehubIssue.state == "open",
124 )
125 ) or 0
126
127 domain_viewer_type = await _resolve_domain_viewer_type(db, getattr(row, "domain_id", None))
128
129 nav_ctx: JSONObject = {
130 "repo_tags": row.tags or [],
131 "repo_visibility": row.visibility or "private",
132 "nav_open_proposal_count": proposal_count,
133 "nav_open_issue_count": issue_count,
134 "nav_domain_viewer_type": domain_viewer_type,
135 }
136 return repo_id, f"/{owner}/{repo_slug}", nav_ctx
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago