Skip to content
Open
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
18 changes: 16 additions & 2 deletions api/github/github_service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import logging
from typing import Dict, Any, List
from cachetools import TTLCache
from db.db import get_db
from common.utils.github import create_issue, get_issues

logger = logging.getLogger("api.github.github_service")
logger.setLevel(logging.DEBUG)

# Live issue reads are hit by the public project pages, so cache per
# (org, repo, state) to protect the shared GITHUB_TOKEN rate budget.
# Only successful responses are cached — errors stay uncached so transient
# GitHub failures retry on the next request.
_ISSUES_CACHE = TTLCache(maxsize=512, ttl=600)

def get_github_organization_data(org_name: str) -> Dict[str, Any]:
"""
Get GitHub organization data including repositories and contributors.
Expand Down Expand Up @@ -360,16 +367,23 @@ def get_github_issues(org_name: str, repo_name: str, state: str ) -> Dict[str, A
if not org_name or not repo_name:
return {"error": "Organization name and repository name are required"}

cache_key = (org_name, repo_name, state)
cached_result = _ISSUES_CACHE.get(cache_key)
if cached_result is not None:
return cached_result

# Get issues using the utility function
issues = get_issues(org_name=org_name, repo_name=repo_name, state=state)

if "error" in issues:
return {"error": issues["error"]}

return {
result = {
"success": True,
"issues": issues
}
_ISSUES_CACHE[cache_key] = result
return result

except Exception as e:
logger.error("Error getting GitHub issues for %s/%s: %s", org_name, repo_name, e)
Expand Down
83 changes: 82 additions & 1 deletion api/teams/teams_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,81 @@ def get_teams_by_hackathon_id(hackathon_id):
logger.error(f"Error getting teams for hackathon {hackathon_id}: {str(e)}")
return {"teams": []}


def _normalize_repo_link(link):
return (link or "").strip().rstrip("/").lower()


def _link_repo_to_problem_statements(db, team_data, nonprofit, repo_name, repo_url):
"""Append the team's newly created GitHub repo to the `github` array of the
problem statement(s) the team is working on, so the public /project/<id>
page shows the code. Historically the repo was only written to the team
doc, leaving most problem statements with an empty `github` field.

Prefers the team's own `problem_statements` refs; falls back to the
nonprofit's problem statements only when there is exactly one (anything
else is ambiguous — skip and log). Never raises: repo linkage is
best-effort and must not fail the approval flow.
"""
try:
ps_refs = []
for ps in team_data.get("problem_statements") or []:
if isinstance(ps, str):
ps_refs.append(db.collection("problem_statements").document(ps))
elif ps is not None and hasattr(ps, "get"): # DocumentReference
ps_refs.append(ps)

if not ps_refs:
# nonprofit comes from get_single_npo → doc_to_json, so entries are id strings
npo_ps = (nonprofit or {}).get("problem_statements") or []
if len(npo_ps) == 1 and isinstance(npo_ps[0], str):
ps_refs.append(db.collection("problem_statements").document(npo_ps[0]))
else:
logger.info(
"Not linking repo %s to a problem statement: team has no "
"problem_statements and nonprofit has %d (ambiguous)",
repo_url, len(npo_ps),
)
return

for ps_ref in ps_refs:
ps_doc = ps_ref.get()
if not ps_doc.exists:
logger.warning("Problem statement %s not found; skipping repo link", ps_ref.id)
continue
ps_data = ps_doc.to_dict() or {}
existing = ps_data.get("github")
if isinstance(existing, list):
github_list = list(existing)
elif isinstance(existing, str) and existing.strip():
# Legacy shape: a bare URL string — convert to the {name, link} form
legacy_link = existing.strip()
github_list = [{"name": legacy_link.rstrip("/").split("/")[-1], "link": legacy_link}]
else:
github_list = []

already_linked = {
_normalize_repo_link(entry.get("link") if isinstance(entry, dict) else entry)
for entry in github_list
}
if _normalize_repo_link(repo_url) in already_linked:
continue

github_list.append({"name": repo_name, "link": repo_url})
ps_ref.set({"github": github_list}, merge=True)
logger.info("Linked repo %s to problem statement %s", repo_url, ps_ref.id)

# The public single-problem-statement read is cached (10 min) and NOT
# in the shared cache registry — clear it explicitly
try:
from api.messages.messages_service import get_single_problem_statement_old
get_single_problem_statement_old.cache_clear()
except Exception:
logger.warning("Could not clear problem statement cache", exc_info=True)
except Exception:
logger.error("Failed linking repo %s to problem statement(s)", repo_url, exc_info=True)


def approve_team(admin_user_id, json):
"""
Admin function to approve a team and pair with nonprofit
Expand Down Expand Up @@ -929,7 +1004,13 @@ def approve_team(admin_user_id, json):
"approved_by": admin_user_id,
"approved_at": datetime.now().isoformat()
}, merge=True)


# Fix-forward: also link the repo to the problem statement(s) so the
# public project page shows the code (best-effort, never blocks approval)
_link_repo_to_problem_statements(
db, team_data, nonprofit, repo_name, full_github_repo_url
)

# Clear cache
clear_cache()

Expand Down
Loading