Source code for gitlab_overviewer.models.overview_data

  1"""Overview data model implementation.
  2
  3Implements :any:`/specs/spec_table_rendering_ui` §2-3, covering:
  4- Column value computation (§2)
  5- Placeholder handling (§3)
  6"""
  7
  8from __future__ import annotations
  9
 10from datetime import datetime
 11from typing import Any, List, Optional
 12
 13from pydantic import BaseModel, Field, field_validator
 14
 15from ..rendering.renderer_base import Renderer
 16
 17from .group import Group
 18from .project import Project
 19from .issue import Issue
 20from .readme import Readme
 21from .readme_extract import ReadmeExtract
 22
 23
[docs] 24class OverviewData(BaseModel): 25 """Container for all data related to a project overview. 26 27 Implements :any:`/specs/spec_table_rendering_ui` §2, providing: 28 29 * Computed column values (repo, type, priority, urgency, etc.) 30 * Formatted cell values (dates, star ratings, issue counts) 31 * Placeholder handling for missing data 32 33 """ 34 35 group: Group 36 project: Project 37 readme: Readme | None = None 38 issues: Optional[List[Issue]] = None 39 extra: ReadmeExtract = Field(default_factory=lambda: ReadmeExtract()) 40
[docs] 41 def model_dump(self, **kwargs) -> dict[str, Any]: 42 """Convert to dict with computed fields for rendering.""" 43 group_data = self.group.model_dump() 44 project_data = self.project.model_dump() 45 46 # Add computed fields from extra data 47 result = { 48 "group": group_data.get("name", "Unknown"), 49 "repo": project_data.get("name", "Unknown"), 50 "type": self.get_type(), 51 "priority": self.get_priority(), 52 "urgency": self.get_urgency(), 53 "date": self.get_date(), 54 "supervisors": self.get_supervisors(), 55 "status": self.get_status(), 56 "todos": self.get_todos(), 57 "version": project_data.get("version", "-"), 58 } 59 60 # Add computed star rating fields 61 result["priority_stars"] = self.get_priority_stars() 62 result["urgency_stars"] = self.get_urgency_stars() 63 result["importance_stars"] = self.get_importance_stars() 64 65 # Add issue count formatting 66 result["issue_summary"] = self.get_issue_summary() 67 68 # Add web_url if available 69 web_url = project_data.get("web_url") 70 if web_url: 71 result["web_url"] = web_url 72 readme_url = project_data.get("readme_url") 73 if readme_url: 74 result["readme_url"] = readme_url 75 76 return result
77
[docs] 78 def get_repo(self) -> str: 79 """Get repository name.""" 80 return self.project.name
81
[docs] 82 def get_supervisors(self) -> str: 83 """Get supervisors from extra data.""" 84 # Prefer supervisors, fallback to authors, else dash 85 if self.extra.supervisors: 86 return "; ".join(self.extra.supervisors) 87 elif self.extra.authors: 88 return "Authors: " + "; ".join(self.extra.authors) 89 return "-"
90
[docs] 91 def get_status(self) -> str: 92 """Get status from extra data.""" 93 # First, try explicit status in front-matter 94 if self.extra.raw_frontmatter and "status" in self.extra.raw_frontmatter: 95 val = self.extra.raw_frontmatter["status"] 96 if isinstance(val, str) and val.strip(): 97 return val.strip() 98 99 # Fall back to heuristic based on open issues 100 if self.issues is not None: 101 open_count = sum(1 for i in self.issues if i.state == "opened") 102 closed_count = sum(1 for i in self.issues if i.state == "closed") 103 if open_count == 0 and closed_count > 0: 104 return "done" 105 if open_count > 0 and closed_count == 0: 106 return "in progress" 107 if open_count > 0 and closed_count > 0: 108 return "mixed" 109 110 # Otherwise dash placeholder 111 return "-"
112
[docs] 113 def get_todos(self) -> str: 114 """Get todos from extra data.""" 115 # Prefer issue-based summary if issues are present 116 if self.issues is not None: 117 summary = self.get_issue_summary() 118 if summary != "—": # m-dash placeholder from get_issue_summary 119 return summary 120 121 # Fallback: derive from README TODO section length 122 if self.readme and self.readme.todo: 123 lines = [ln for ln in self.readme.todo.splitlines() if ln.strip()] 124 if lines: 125 return f"from README: {len(lines)} lines" 126 127 return "-"
128
[docs] 129 def get_type(self) -> str: 130 """Get type from extra data.""" 131 # Try to get from raw_frontmatter 132 if self.extra.raw_frontmatter and "type" in self.extra.raw_frontmatter: 133 return str(self.extra.raw_frontmatter["type"]) 134 return "-"
135
[docs] 136 def get_priority(self) -> int | None: 137 """Get priority from extra data.""" 138 if self.extra.raw_frontmatter and "priority" in self.extra.raw_frontmatter: 139 return Renderer.safe_int(self.extra.raw_frontmatter["priority"], 0) 140 return 0
141
[docs] 142 def get_urgency(self) -> int | None: 143 """Get urgency from extra data.""" 144 if self.extra.raw_frontmatter and "urgency" in self.extra.raw_frontmatter: 145 return Renderer.safe_int(self.extra.raw_frontmatter["urgency"], 0) 146 return 0
147
[docs] 148 def get_importance(self) -> int | None: 149 """Get importance from extra data.""" 150 if self.extra.raw_frontmatter and "importance" in self.extra.raw_frontmatter: 151 return Renderer.safe_int(self.extra.raw_frontmatter["importance"], 0) 152 return 0
153
[docs] 154 def get_priority_stars(self) -> str: 155 """Get priority as star rating.""" 156 priority = self.get_priority() 157 return Renderer.star_rating(priority) if priority is not None else "—"
158
[docs] 159 def get_urgency_stars(self) -> str: 160 """Get urgency as star rating.""" 161 urgency = self.get_urgency() 162 return Renderer.star_rating(urgency) if urgency is not None else "—"
163
[docs] 164 def get_importance_stars(self) -> str: 165 """Get importance as star rating.""" 166 importance = self.get_importance() 167 return Renderer.star_rating(importance) if importance is not None else "—"
168
[docs] 169 def get_issue_summary(self) -> str: 170 """Format issue counts for display.""" 171 if not self.issues: 172 return "—" 173 174 open_count = sum(1 for issue in self.issues if issue.state == "opened") 175 closed_count = sum(1 for issue in self.issues if issue.state == "closed") 176 177 if open_count == 0 and closed_count == 0: 178 return "—" 179 180 parts = [] 181 if closed_count > 0: 182 parts.append(f"☑: {closed_count}") 183 if open_count > 0: 184 parts.append(f"☒: {open_count}") 185 186 return "; ".join(parts)
187
[docs] 188 def get_date(self) -> str: 189 """Get formatted date from project or extra data.""" 190 if self.extra.raw_frontmatter and "date" in self.extra.raw_frontmatter: 191 return str(self.extra.raw_frontmatter["date"]) 192 # Try to get from project's last_activity_at 193 if self.project.last_activity_at: 194 try: 195 dt = Renderer.parse_iso_date(self.project.last_activity_at) 196 if dt: 197 return dt.strftime("%Y-%m-%d") 198 except Exception: 199 pass 200 return "-"
201 202 model_config = {"extra": "ignore"}
203 204 205__all__ = ["OverviewData"] 206 207OverviewData.model_rebuild()