88
99from __future__ import annotations
1010
11+ from datetime import datetime
12+ from typing import Any
13+
1114from pydantic import Field
1215
1316from glpi_python_client .models ._base import GlpiModel
17+ from glpi_python_client .models .api_schema ._common import IdNameRef
1418from glpi_python_client .models .api_schema .assistance ._ticket import GetTicket
1519from glpi_python_client .models .api_schema .assistance .timeline ._document import (
1620 GetTimelineDocument ,
2428from glpi_python_client .models .api_schema .assistance .timeline ._task import (
2529 GetTicketTask ,
2630)
31+ from glpi_python_client .models .api_schema .enums import GlpiTimelinePosition
32+
33+ _MAX_DATETIME = datetime .max
34+
35+
36+ def _ref_label (ref : IdNameRef | None ) -> str | None :
37+ """Return the human-readable label of one ``IdNameRef`` reference.
38+
39+ The helper prefers ``name`` (the GLPI display label) and falls back to
40+ the numeric identifier when the server only returned the foreign key.
41+ Returns ``None`` when the reference itself is missing so callers can
42+ omit the field from the rendered Markdown.
43+ """
44+
45+ if ref is None :
46+ return None
47+ if ref .name :
48+ return ref .name
49+ if ref .id is not None :
50+ return f"#{ ref .id } "
51+ return None
52+
53+
54+ def _event_sort_key (event : Any ) -> tuple [int , int , datetime ]:
55+ """Compute the sort key used to order timeline events for rendering.
56+
57+ Events with a meaningful ``timeline_position`` (a positive
58+ :class:`GlpiTimelinePosition` member) come first in position order so
59+ the rendered transcript matches the GLPI UI layout. The remaining
60+ events fall back to ``date_creation``; events missing both attributes
61+ are pushed to the end with a stable ordering.
62+ """
63+
64+ position = getattr (event , "timeline_position" , None )
65+ fallback_date = getattr (event , "date_creation" , None ) or _MAX_DATETIME
66+ if isinstance (position , GlpiTimelinePosition ) and position .value > 0 :
67+ return (0 , position .value , fallback_date )
68+ return (1 , 0 , fallback_date )
2769
2870
2971class GlpiTicketContext (GlpiModel ):
@@ -49,5 +91,76 @@ class GlpiTicketContext(GlpiModel):
4991 solutions : list [GetSolution ] = Field (default_factory = list )
5092 documents : list [GetTimelineDocument ] = Field (default_factory = list )
5193
94+ def to_markdown (self ) -> str :
95+ """Render the ticket and its timeline as one Markdown transcript.
96+
97+ The header reports the ticket identifier, name, status, and body,
98+ followed by every timeline event (followups, tasks, solutions)
99+ sorted by ``timeline_position`` when set and otherwise by
100+ ``date_creation``. Linked documents are appended at the end as a
101+ bullet list because the timeline document link payload does not
102+ carry a creation timestamp. Empty fields are omitted so the
103+ output stays compact regardless of how complete the GLPI payload
104+ is.
105+
106+ Returns
107+ -------
108+ str
109+ Markdown transcript suitable for direct display or for
110+ forwarding into a downstream Markdown renderer. The string
111+ never ends with trailing whitespace.
112+ """
113+
114+ lines : list [str ] = []
115+ ticket = self .ticket
116+ ticket_label = ticket .name or "(unnamed ticket)"
117+ if ticket .id is not None :
118+ lines .append (f"# Ticket #{ ticket .id } \u2014 { ticket_label } " )
119+ else :
120+ lines .append (f"# Ticket \u2014 { ticket_label } " )
121+ status_label = _ref_label (ticket .status )
122+ if status_label is not None :
123+ lines .append (f"- **Status**: { status_label } " )
124+ if ticket .content :
125+ lines .append ("" )
126+ lines .append (ticket .content )
127+
128+ events : list [tuple [str , Any ]] = []
129+ events .extend (("Followup" , item ) for item in self .followups )
130+ events .extend (("Task" , item ) for item in self .tasks )
131+ events .extend (("Solution" , item ) for item in self .solutions )
132+ events .sort (key = lambda pair : _event_sort_key (pair [1 ]))
133+
134+ for kind , event in events :
135+ event_id = getattr (event , "id" , None )
136+ heading = f"## { kind } #{ event_id } " if event_id is not None else f"## { kind } "
137+ lines .append ("" )
138+ lines .append (heading )
139+ author_label = _ref_label (getattr (event , "user" , None ))
140+ if author_label is not None :
141+ lines .append (f"- **Author**: { author_label } " )
142+ created = getattr (event , "date_creation" , None )
143+ if created is not None :
144+ lines .append (f"- **Created**: { created .isoformat ()} " )
145+ duration = getattr (event , "duration" , None )
146+ if duration is not None :
147+ lines .append (f"- **Duration**: { duration } s" )
148+ content = getattr (event , "content" , None )
149+ if content :
150+ lines .append ("" )
151+ lines .append (content )
152+
153+ if self .documents :
154+ lines .append ("" )
155+ lines .append ("## Documents" )
156+ for document in self .documents :
157+ identifier = document .documents_id or document .id
158+ label = document .filepath or (
159+ f"document #{ identifier } " if identifier is not None else "document"
160+ )
161+ lines .append (f"- { label } " )
162+
163+ return "\n " .join (lines ).rstrip ()
164+
52165
53166__all__ = ["GlpiTicketContext" ]
0 commit comments