_nav_ctx.py
python
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