Coverage for rfpy/api/io/excel_import.py: 99%
75 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-31 16:00 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-31 16:00 +0000
1from typing import Tuple, Mapping
3from openpyxl import load_workbook
4from openpyxl.worksheet.worksheet import Worksheet
5from openpyxl.cell.read_only import ReadOnlyCell
7from rfpy.model import Project
8from rfpy.model.questionnaire import (
9 Section, RadioChoices, TextInput, Label, QuestionInstance, QuestionDefinition
10)
13class XLCell():
15 def __init__(self, col_idx: int):
16 self.col_idx = col_idx
18 def __get__(self, obj: 'QRow', type=None) -> object:
19 return obj.row[self.col_idx].value
22class QRow:
24 section_title = XLCell(0)
25 question_title = XLCell(1)
26 question_text = XLCell(2)
27 multi_choices = XLCell(3)
28 comments_title = XLCell(4)
30 def __init__(self, row: Tuple[ReadOnlyCell]):
31 self.row = row
33 def is_root_question(self):
34 return not self.section_title
36 def choices_list(self):
37 cd = []
38 for choice_str in self.multi_choices.split(';'):
39 choice, autoscore = RadioChoices.split_choice_string(choice_str)
40 cd.append(
41 {"label": choice, "autoscore": autoscore}
42 )
43 if len(cd) < 2:
44 raise ValueError("At least two values are required for multiple choice options")
45 return cd
48def row_to_question(qrow) -> QuestionDefinition:
49 qdef = QuestionDefinition(title=qrow.question_title)
50 elems = qdef.elements
51 elems.append(Label(label=qrow.question_text, row=1, col=1))
52 if qrow.multi_choices:
53 choices = RadioChoices(choices=qrow.choices_list(), row=2, col=1)
54 elems.append(choices)
55 if qrow.comments_title:
56 elems.append(Label(label=qrow.comments_title, row=3, col=1))
57 elems.append(TextInput(height=5, width=60, row=4, col=1))
58 else:
59 if qrow.comments_title:
60 elems.append(Label(label=qrow.comments_title, row=2, col=1))
61 elems.append(TextInput(height=5, width=60, row=3, col=1))
62 else:
63 elems.append(TextInput(height=5, width=60, row=2, col=1))
64 return qdef
67class ExcelQImporter:
69 def __init__(self, parent_section: Section) -> None:
70 self.created_count = 0
71 if parent_section is None:
72 raise ValueError('missing required Section object, parent_section')
73 self.parent_section = parent_section
74 self.project: Project = parent_section.project
75 self.created_sections: Mapping[str: Section] = {}
77 def read_questions_excel(self, filename):
78 wb = load_workbook(filename=filename, read_only=True)
79 try:
80 self.process_rows(wb['Sheet1'])
81 finally:
82 wb.close()
84 def process_rows(self, ws: Worksheet):
85 # max_row is 501 to allow a maximum of 500 rows since
86 # rows are indexed from 1, not zero, and there is a header row
87 for row in ws.iter_rows(min_row=2, max_row=501):
88 qr = QRow(row)
89 if qr.question_title:
90 self.process_one_row(qr)
91 self.created_count += 1
93 def process_one_row(self, qrow: QRow):
94 qdef = row_to_question(qrow)
95 qi = QuestionInstance(question_def=qdef, project=self.project)
96 sec: Section = None
97 if qrow.is_root_question():
98 sec = self.parent_section
99 elif qrow.section_title in self.created_sections:
100 sec = self.created_sections[qrow.section_title]
101 else:
102 sec = Section(title=qrow.section_title, project=self.project)
103 self.parent_section.subsections.append(sec)
104 self.created_sections[qrow.section_title] = sec
105 sec.questions.append(qi)