Coverage for postrfp/buyer/api/io/xml/importer.py: 100%
75 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 21:34 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 21:34 +0000
1from lxml import etree
2from sqlalchemy.orm import Session
3import logging
5from postrfp.model import Section
6from postrfp.buyer.api.io.qbuilder import QuestionnaireBuilder, parse_bool_attr
8logger = logging.getLogger(__name__)
11class XmlQuestionnaireImporter:
12 def __init__(self, session: Session, root: Section):
13 self.root_section = root
14 self.session = session
15 self.qb = QuestionnaireBuilder(session, root)
16 self.current_td: dict[str, int] | None = (
17 None # Minimal state for table cell context only
18 )
19 self.current_question: etree._Element | None = None
20 self.current_description: str | None = None # Store description text
22 def dispatch(self, event: str, element: etree._Element) -> None:
23 handler_name = f"{event}_{element.tag}"
24 handler = getattr(self, handler_name, None)
25 if handler is None:
26 return
28 handler(element)
30 def parse(self, filename: str) -> Section:
31 """Parse an XML file and build the questionnaire structure."""
32 self.current_question = None
33 self.current_td = None
35 with self.qb: # Let QuestionnaireBuilder manage the overall context
36 for event, element in etree.iterparse(filename, events=("start", "end")):
37 self.dispatch(event, element)
39 return self.root_section
41 # Section handlers
43 def start_section(self, section_element: etree._Element) -> None:
44 title = section_element.get("title", "Untitled Section")
45 # Start section with empty description, we'll update it later
46 self.qb.start_section(title, None)
47 self.current_description = None
49 def end_section(self, section_element: etree._Element) -> None:
50 # Pass the description to end_section
51 self.qb.end_section(description=self.current_description)
52 self.current_description = None
53 section_element.clear()
55 # Add handler for description element
56 def start_description(self, element: etree._Element) -> None:
57 # Store the description text for the next end_section call
58 self.current_description = element.text
60 # Question handlers
61 def start_question(self, element: etree._Element) -> None:
62 """Start a new question context."""
63 title = element.get("title", "")
64 self.qb.start_question(title)
65 self.current_question = element
67 def end_question(self, element: etree._Element) -> None:
68 """End the current question context."""
69 self.qb.end_question()
70 self.current_question = None
72 def start_td(self, element: etree._Element) -> None:
73 """Process a table cell."""
75 # Set current td with colspan/rowspan attributes
76 self.current_td = {
77 "colspan": int(element.get("colspan", 1)),
78 "rowspan": int(element.get("rowspan", 1)),
79 }
81 def end_td(self, element: etree._Element) -> None:
82 """End the current table cell."""
83 self.current_td = None
85 # Element handlers within table cells
86 def start_text(self, text_element: etree._Element) -> None:
87 assert self.current_td is not None, (
88 "start_text called outside of table cell context"
89 )
90 self.qb.add_text_input(
91 label=text_element.get("label", ""),
92 colspan=self.current_td["colspan"],
93 rowspan=self.current_td["rowspan"],
94 mandatory=parse_bool_attr(text_element.get("mandatory")),
95 width=int(text_element.get("width", 40)),
96 height=int(text_element.get("height", 1)),
97 )
99 def start_checkbox(self, checkbox_element: etree._Element) -> None:
100 assert self.current_td is not None, (
101 "start_checkbox called outside of table cell context"
102 )
103 self.qb.add_checkbox(
104 label=checkbox_element.get("label", ""),
105 colspan=self.current_td["colspan"],
106 rowspan=self.current_td["rowspan"],
107 )
109 def start_label(self, label_element: etree._Element) -> None:
110 assert self.current_td is not None, (
111 "start_label called outside of table cell context"
112 )
113 self.qb.add_label(
114 label=label_element.text or "",
115 colspan=self.current_td["colspan"],
116 rowspan=self.current_td["rowspan"],
117 )
119 # Attachment handlers
120 def start_attachment(self, attachment_element: etree._Element) -> None:
121 assert self.current_td is not None, (
122 "start_attachment called outside of table cell context"
123 )
124 self.qb.add_attachment(
125 label=attachment_element.get("label", ""),
126 colspan=self.current_td["colspan"],
127 rowspan=self.current_td["rowspan"],
128 mandatory=parse_bool_attr(attachment_element.get("mandatory")),
129 )
131 def start_questionattachment(self, qa_element: etree._Element) -> None:
132 assert self.current_td is not None, (
133 "start_questionattachment called outside of table cell context"
134 )
135 # Convert to a simple label instead
136 filename = qa_element.get("filename", "")
137 label = qa_element.get("label", "") or f"Attachment: {filename}"
139 self.qb.add_label(
140 label=label,
141 colspan=self.current_td["colspan"],
142 rowspan=self.current_td["rowspan"],
143 )
145 # Choice handlers
146 def start_choiceset(self, choiceset_element: etree._Element) -> None:
147 assert self.current_td is not None, (
148 "start_choiceset called outside of table cell context"
149 )
150 is_combo = choiceset_element.get("type", "combobox") == "combobox"
152 # Use the new context management in QBuilder
153 self.qb.start_choice_set(
154 label=choiceset_element.get("label", ""),
155 is_combo=is_combo,
156 colspan=self.current_td["colspan"],
157 rowspan=self.current_td["rowspan"],
158 mandatory=parse_bool_attr(choiceset_element.get("mandatory")),
159 )
161 def end_choiceset(self, choiceset_element: etree._Element) -> None:
162 # Let QBuilder handle the choice set completion
163 self.qb.end_choice_set()
165 def start_choice(self, choice_element: etree._Element) -> None:
166 value = choice_element.get("value", "")
167 label = choice_element.get("label", value)
169 # Add the choice to the current choice set in QBuilder
170 self.qb.add_choice(value, label)