scriptplan 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scriptplan/__init__.py +22 -0
- scriptplan/cli/__init__.py +7 -0
- scriptplan/cli/main.py +546 -0
- scriptplan/core/__init__.py +0 -0
- scriptplan/core/account.py +125 -0
- scriptplan/core/allocation.py +69 -0
- scriptplan/core/booking.py +39 -0
- scriptplan/core/journal.py +377 -0
- scriptplan/core/leave.py +14 -0
- scriptplan/core/limits.py +354 -0
- scriptplan/core/project.py +924 -0
- scriptplan/core/property.py +1290 -0
- scriptplan/core/resource.py +198 -0
- scriptplan/core/resource_scenario.py +711 -0
- scriptplan/core/scenario.py +5 -0
- scriptplan/core/scenario_data.py +39 -0
- scriptplan/core/shift.py +71 -0
- scriptplan/core/task.py +77 -0
- scriptplan/core/task_scenario.py +1515 -0
- scriptplan/core/timesheet.py +457 -0
- scriptplan/core/working_hours.py +231 -0
- scriptplan/parser/__init__.py +0 -0
- scriptplan/parser/macro_processor.py +264 -0
- scriptplan/parser/tjp.lark +412 -0
- scriptplan/parser/tjp_parser.py +1904 -0
- scriptplan/py.typed +0 -0
- scriptplan/report/__init__.py +75 -0
- scriptplan/report/html_generator.py +477 -0
- scriptplan/report/report.py +466 -0
- scriptplan/report/report_base.py +397 -0
- scriptplan/report/report_context.py +248 -0
- scriptplan/report/resource_report.py +341 -0
- scriptplan/report/table_report.py +693 -0
- scriptplan/report/task_report.py +362 -0
- scriptplan/report/text_report.py +172 -0
- scriptplan/scheduler/__init__.py +0 -0
- scriptplan/scheduler/batch_processor.py +238 -0
- scriptplan/scheduler/scoreboard.py +120 -0
- scriptplan/utils/__init__.py +0 -0
- scriptplan/utils/data_cache.py +46 -0
- scriptplan/utils/logger.py +243 -0
- scriptplan/utils/message_handler.py +515 -0
- scriptplan/utils/time.py +195 -0
- scriptplan-0.9.0.dist-info/METADATA +161 -0
- scriptplan-0.9.0.dist-info/RECORD +49 -0
- scriptplan-0.9.0.dist-info/WHEEL +5 -0
- scriptplan-0.9.0.dist-info/entry_points.txt +2 -0
- scriptplan-0.9.0.dist-info/licenses/LICENSE +201 -0
- scriptplan-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Report - Base class for all report types.
|
|
3
|
+
|
|
4
|
+
This module implements the Report class which holds the fundamental description
|
|
5
|
+
and functionality to turn a scheduled project into user-readable form.
|
|
6
|
+
A report may contain other reports (nested reports).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import TYPE_CHECKING, Optional, List, Any, Dict
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from scriptplan.core.property import PropertyTreeNode
|
|
15
|
+
from scriptplan.core.scenario_data import ScenarioData
|
|
16
|
+
from scriptplan.utils.message_handler import MessageHandler
|
|
17
|
+
from scriptplan.report.report_context import ReportContext
|
|
18
|
+
from scriptplan.report.html_generator import (
|
|
19
|
+
build_html_document, get_default_css
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from scriptplan.core.project import Project
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReportFormat(Enum):
|
|
27
|
+
"""Supported report output formats."""
|
|
28
|
+
HTML = 'html'
|
|
29
|
+
CSV = 'csv'
|
|
30
|
+
ICAL = 'ical'
|
|
31
|
+
TJP = 'tjp'
|
|
32
|
+
CTAGS = 'ctags'
|
|
33
|
+
NIKU = 'niku'
|
|
34
|
+
MSPXML = 'mspxml'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ReportType(Enum):
|
|
38
|
+
"""Supported report types."""
|
|
39
|
+
TASK_REPORT = 'taskreport'
|
|
40
|
+
RESOURCE_REPORT = 'resourcereport'
|
|
41
|
+
ACCOUNT_REPORT = 'accountreport'
|
|
42
|
+
TEXT_REPORT = 'textreport'
|
|
43
|
+
TRACE_REPORT = 'tracereport'
|
|
44
|
+
STATUS_SHEET = 'statusSheet'
|
|
45
|
+
TIME_SHEET = 'timeSheet'
|
|
46
|
+
ICAL = 'iCal'
|
|
47
|
+
NIKU = 'niku'
|
|
48
|
+
EXPORT = 'export'
|
|
49
|
+
TAG_FILE = 'tagfile'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ReportScenario(ScenarioData):
|
|
53
|
+
"""
|
|
54
|
+
Dummy class to make the 'flags' attribute work for reports.
|
|
55
|
+
Reports don't have scenario-specific attributes but need this
|
|
56
|
+
for consistent flag handling.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Report(PropertyTreeNode, MessageHandler):
|
|
62
|
+
"""
|
|
63
|
+
Base class for all reports.
|
|
64
|
+
|
|
65
|
+
The Report class holds the fundamental description and functionality to
|
|
66
|
+
turn the scheduled project into a user readable form. A report may contain
|
|
67
|
+
other reports (nested reports).
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
type_spec: The type of report (task, resource, text, etc.)
|
|
71
|
+
content: The generated content object (TaskListRE, ResourceListRE, etc.)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, project: 'Project', id: str, name: str, parent: Optional['Report'] = None):
|
|
75
|
+
"""
|
|
76
|
+
Create a new Report object.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
project: The Project object
|
|
80
|
+
id: Unique identifier for this report
|
|
81
|
+
name: Display name (also used as filename)
|
|
82
|
+
parent: Optional parent report for nested reports
|
|
83
|
+
"""
|
|
84
|
+
super().__init__(project.reports, id, name, parent)
|
|
85
|
+
|
|
86
|
+
self._check_filename(name)
|
|
87
|
+
project.addReport(self)
|
|
88
|
+
|
|
89
|
+
# The type specifier must be set for every report
|
|
90
|
+
self.type_spec: Optional[ReportType] = None
|
|
91
|
+
|
|
92
|
+
# The generated content object
|
|
93
|
+
self.content: Optional[Any] = None
|
|
94
|
+
|
|
95
|
+
# Reports need scenario data for flag handling
|
|
96
|
+
scenario_count = project.scenarioCount()
|
|
97
|
+
self.data = [None] * scenario_count
|
|
98
|
+
for i in range(scenario_count):
|
|
99
|
+
self.data[i] = ReportScenario(self, i, self._scenarioAttributes[i])
|
|
100
|
+
|
|
101
|
+
def _check_filename(self, name: str) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Validate the filename for the report.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
name: The filename to validate
|
|
107
|
+
"""
|
|
108
|
+
if not name:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Check for invalid characters
|
|
112
|
+
invalid_chars = ['<', '>', ':', '"', '|', '?', '*']
|
|
113
|
+
for char in invalid_chars:
|
|
114
|
+
if char in name:
|
|
115
|
+
self.error('invalid_filename',
|
|
116
|
+
f"Report filename '{name}' contains invalid character '{char}'")
|
|
117
|
+
|
|
118
|
+
def generate(self, requested_formats: Optional[List[ReportFormat]] = None) -> int:
|
|
119
|
+
"""
|
|
120
|
+
Generate the report in the requested formats.
|
|
121
|
+
|
|
122
|
+
This is where the main action happens. The report defined by all class
|
|
123
|
+
attributes and report elements is generated according to the requested
|
|
124
|
+
output format(s).
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
requested_formats: List of formats to generate. If None, uses
|
|
128
|
+
formats specified in report definition.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
0 on success, non-zero on error
|
|
132
|
+
"""
|
|
133
|
+
# Store current timezone and set report timezone
|
|
134
|
+
# old_timezone = TjTime.setTimeZone(self.get('timezone'))
|
|
135
|
+
|
|
136
|
+
# Generate intermediate format first
|
|
137
|
+
self.generate_intermediate_format()
|
|
138
|
+
|
|
139
|
+
# Determine which formats to generate
|
|
140
|
+
formats = requested_formats or self.get('formats') or []
|
|
141
|
+
|
|
142
|
+
for fmt in formats:
|
|
143
|
+
if not self.name:
|
|
144
|
+
self.error('empty_report_file_name',
|
|
145
|
+
f"Report {self.id} has output formats requested, "
|
|
146
|
+
"but the file name is empty.")
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if fmt == ReportFormat.ICAL:
|
|
150
|
+
self._generate_ical()
|
|
151
|
+
elif fmt == ReportFormat.HTML:
|
|
152
|
+
self._generate_html()
|
|
153
|
+
self._copy_auxiliary_files()
|
|
154
|
+
elif fmt == ReportFormat.CSV:
|
|
155
|
+
self._generate_csv()
|
|
156
|
+
elif fmt == ReportFormat.CTAGS:
|
|
157
|
+
self._generate_ctags()
|
|
158
|
+
elif fmt == ReportFormat.NIKU:
|
|
159
|
+
self._generate_niku()
|
|
160
|
+
elif fmt == ReportFormat.TJP:
|
|
161
|
+
self._generate_tjp()
|
|
162
|
+
elif fmt == ReportFormat.MSPXML:
|
|
163
|
+
self._generate_msp_xml()
|
|
164
|
+
else:
|
|
165
|
+
raise ValueError(f"Unknown report output format {fmt}")
|
|
166
|
+
|
|
167
|
+
# Restore timezone
|
|
168
|
+
# TjTime.setTimeZone(old_timezone)
|
|
169
|
+
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
def generate_intermediate_format(self) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Generate an output format agnostic version.
|
|
175
|
+
|
|
176
|
+
This intermediate format can later be turned into the respective
|
|
177
|
+
output formats (HTML, CSV, etc.).
|
|
178
|
+
"""
|
|
179
|
+
# scenarios = self.get('scenarios') or []
|
|
180
|
+
# if not scenarios:
|
|
181
|
+
# self.warning('all_scenarios_disabled',
|
|
182
|
+
# f"The report {self.fullId} has only disabled scenarios. "
|
|
183
|
+
# "The report will possibly be empty.")
|
|
184
|
+
|
|
185
|
+
self.content = None
|
|
186
|
+
|
|
187
|
+
# Import report type classes here to avoid circular imports
|
|
188
|
+
if self.type_spec == ReportType.TASK_REPORT:
|
|
189
|
+
from scriptplan.report.task_report import TaskReport
|
|
190
|
+
self.content = TaskReport(self)
|
|
191
|
+
elif self.type_spec == ReportType.RESOURCE_REPORT:
|
|
192
|
+
from scriptplan.report.resource_report import ResourceReport
|
|
193
|
+
self.content = ResourceReport(self)
|
|
194
|
+
elif self.type_spec == ReportType.TEXT_REPORT:
|
|
195
|
+
from scriptplan.report.text_report import TextReport
|
|
196
|
+
self.content = TextReport(self)
|
|
197
|
+
elif self.type_spec == ReportType.ACCOUNT_REPORT:
|
|
198
|
+
# from scriptplan.report.account_report import AccountReport
|
|
199
|
+
# self.content = AccountReport(self)
|
|
200
|
+
pass
|
|
201
|
+
elif self.type_spec == ReportType.TRACE_REPORT:
|
|
202
|
+
# from scriptplan.report.trace_report import TraceReport
|
|
203
|
+
# self.content = TraceReport(self)
|
|
204
|
+
pass
|
|
205
|
+
elif self.type_spec == ReportType.STATUS_SHEET:
|
|
206
|
+
# from scriptplan.report.status_sheet_report import StatusSheetReport
|
|
207
|
+
# self.content = StatusSheetReport(self)
|
|
208
|
+
pass
|
|
209
|
+
elif self.type_spec == ReportType.TIME_SHEET:
|
|
210
|
+
# from scriptplan.report.time_sheet_report import TimeSheetReport
|
|
211
|
+
# self.content = TimeSheetReport(self)
|
|
212
|
+
pass
|
|
213
|
+
elif self.type_spec == ReportType.ICAL:
|
|
214
|
+
# from scriptplan.report.ical_report import ICalReport
|
|
215
|
+
# self.content = ICalReport(self)
|
|
216
|
+
pass
|
|
217
|
+
elif self.type_spec == ReportType.NIKU:
|
|
218
|
+
# from scriptplan.report.niku_report import NikuReport
|
|
219
|
+
# self.content = NikuReport(self)
|
|
220
|
+
pass
|
|
221
|
+
elif self.type_spec == ReportType.EXPORT:
|
|
222
|
+
# from scriptplan.report.export_report import ExportReport
|
|
223
|
+
# self.content = ExportReport(self)
|
|
224
|
+
pass
|
|
225
|
+
elif self.type_spec == ReportType.TAG_FILE:
|
|
226
|
+
# from scriptplan.report.tag_file import TagFile
|
|
227
|
+
# self.content = TagFile(self)
|
|
228
|
+
pass
|
|
229
|
+
else:
|
|
230
|
+
if self.type_spec:
|
|
231
|
+
raise ValueError(f"Unknown report type: {self.type_spec}")
|
|
232
|
+
|
|
233
|
+
# Generate intermediate format for the content
|
|
234
|
+
if self.content:
|
|
235
|
+
self.content.generate_intermediate_format()
|
|
236
|
+
|
|
237
|
+
def to_html(self) -> Optional[str]:
|
|
238
|
+
"""
|
|
239
|
+
Render the content of the report as HTML.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
HTML string or None if no content
|
|
243
|
+
"""
|
|
244
|
+
return self.content.to_html() if self.content else None
|
|
245
|
+
|
|
246
|
+
def to_csv(self) -> Optional[List[List[str]]]:
|
|
247
|
+
"""
|
|
248
|
+
Convert the report to CSV format.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of rows, each row being a list of column values
|
|
252
|
+
"""
|
|
253
|
+
return self.content.to_csv() if self.content else None
|
|
254
|
+
|
|
255
|
+
def interactive(self) -> bool:
|
|
256
|
+
"""
|
|
257
|
+
Check if report should be rendered in interactive version.
|
|
258
|
+
|
|
259
|
+
The top-level report defines the output format and the interactive setting.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if interactive mode, False otherwise
|
|
263
|
+
"""
|
|
264
|
+
if self.project.reportContexts:
|
|
265
|
+
top_report = self.project.reportContexts[0].report
|
|
266
|
+
return top_report.get('interactive') or False
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
def _get_output_path(self, extension: str) -> Path:
|
|
270
|
+
"""
|
|
271
|
+
Get the output file path for a given extension.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
extension: File extension (e.g., 'html', 'csv')
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Full path to output file
|
|
278
|
+
"""
|
|
279
|
+
output_dir = self.project.outputDir or './'
|
|
280
|
+
base_name = self.name or self.id
|
|
281
|
+
return Path(output_dir) / f"{base_name}.{extension}"
|
|
282
|
+
|
|
283
|
+
def _generate_html(self) -> None:
|
|
284
|
+
"""Generate HTML output."""
|
|
285
|
+
if not self.content:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
if not hasattr(self.content, 'to_html'):
|
|
289
|
+
self.warning('html_not_supported',
|
|
290
|
+
f"HTML format is not supported for report {self.id} "
|
|
291
|
+
f"of type {self.type_spec}")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
html_content = self._build_html_document()
|
|
295
|
+
output_path = self._get_output_path('html')
|
|
296
|
+
|
|
297
|
+
os.makedirs(output_path.parent, exist_ok=True)
|
|
298
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
299
|
+
f.write(html_content)
|
|
300
|
+
|
|
301
|
+
def _build_html_document(self) -> str:
|
|
302
|
+
"""
|
|
303
|
+
Build complete HTML document.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Complete HTML document as string
|
|
307
|
+
"""
|
|
308
|
+
title = f"{self.project.name} - {self.get('title') or self.name}"
|
|
309
|
+
body_content = self.content.to_html() if self.content else ""
|
|
310
|
+
if body_content is None:
|
|
311
|
+
body_content = ""
|
|
312
|
+
|
|
313
|
+
# Build navigation for sibling reports
|
|
314
|
+
navigation = self._build_navigation()
|
|
315
|
+
|
|
316
|
+
# Build subtitle from report period
|
|
317
|
+
subtitle = self._build_subtitle()
|
|
318
|
+
|
|
319
|
+
return build_html_document(
|
|
320
|
+
title=title,
|
|
321
|
+
content=body_content,
|
|
322
|
+
project_name=self.project.name,
|
|
323
|
+
subtitle=subtitle,
|
|
324
|
+
navigation=navigation,
|
|
325
|
+
include_css=True,
|
|
326
|
+
footer=True
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _build_navigation(self) -> Optional[List[Dict[str, str]]]:
|
|
330
|
+
"""
|
|
331
|
+
Build navigation links for sibling reports.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
List of navigation items or None
|
|
335
|
+
"""
|
|
336
|
+
# Get all reports in the project
|
|
337
|
+
reports = list(self.project.reports)
|
|
338
|
+
if not reports or len(reports) <= 1:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
navigation = []
|
|
342
|
+
for report in reports:
|
|
343
|
+
# Skip reports without names (they won't have HTML output)
|
|
344
|
+
if not report.name:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
nav_item = {
|
|
348
|
+
'title': report.get('title') or report.name,
|
|
349
|
+
'url': f'{report.name}.html',
|
|
350
|
+
'active': report == self
|
|
351
|
+
}
|
|
352
|
+
navigation.append(nav_item)
|
|
353
|
+
|
|
354
|
+
return navigation if len(navigation) > 1 else None
|
|
355
|
+
|
|
356
|
+
def _build_subtitle(self) -> str:
|
|
357
|
+
"""
|
|
358
|
+
Build subtitle showing report period or other context.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Subtitle string
|
|
362
|
+
"""
|
|
363
|
+
parts = []
|
|
364
|
+
|
|
365
|
+
# Add report period if defined
|
|
366
|
+
start = self.get('start')
|
|
367
|
+
end = self.get('end')
|
|
368
|
+
if start and end:
|
|
369
|
+
from scriptplan.report.html_generator import format_date
|
|
370
|
+
parts.append(f"{format_date(start)} - {format_date(end)}")
|
|
371
|
+
|
|
372
|
+
# Add scenario info if multiple scenarios
|
|
373
|
+
scenarios = self.get('scenarios') or []
|
|
374
|
+
if len(scenarios) > 1:
|
|
375
|
+
scenario_names = []
|
|
376
|
+
for scen in scenarios:
|
|
377
|
+
# Handle both scenario names and indices
|
|
378
|
+
if isinstance(scen, int):
|
|
379
|
+
if scen < len(self.project.scenarios):
|
|
380
|
+
scenario_names.append(self.project.scenarios[scen].name)
|
|
381
|
+
elif isinstance(scen, str):
|
|
382
|
+
# Resolve scenario name to get display name
|
|
383
|
+
for proj_scen in self.project.scenarios:
|
|
384
|
+
if proj_scen.id == scen:
|
|
385
|
+
scenario_names.append(proj_scen.name or scen)
|
|
386
|
+
break
|
|
387
|
+
else:
|
|
388
|
+
scenario_names.append(scen) # Use raw name if not found
|
|
389
|
+
if scenario_names:
|
|
390
|
+
parts.append(f"Scenarios: {', '.join(scenario_names)}")
|
|
391
|
+
|
|
392
|
+
return ' | '.join(parts) if parts else ''
|
|
393
|
+
|
|
394
|
+
def _generate_csv(self) -> None:
|
|
395
|
+
"""Generate CSV output."""
|
|
396
|
+
if not self.content:
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
if not hasattr(self.content, 'to_csv'):
|
|
400
|
+
self.warning('csv_not_supported',
|
|
401
|
+
f"CSV format is not supported for report {self.id} "
|
|
402
|
+
f"of type {self.type_spec}")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
csv_data = self.content.to_csv()
|
|
406
|
+
if not csv_data:
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
output_path = self._get_output_path('csv')
|
|
410
|
+
os.makedirs(output_path.parent, exist_ok=True)
|
|
411
|
+
|
|
412
|
+
import csv
|
|
413
|
+
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
|
414
|
+
writer = csv.writer(f)
|
|
415
|
+
writer.writerows(csv_data)
|
|
416
|
+
|
|
417
|
+
def _generate_ical(self) -> None:
|
|
418
|
+
"""Generate iCal output."""
|
|
419
|
+
# To be implemented
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
def _generate_ctags(self) -> None:
|
|
423
|
+
"""Generate ctags output."""
|
|
424
|
+
# To be implemented
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
def _generate_niku(self) -> None:
|
|
428
|
+
"""Generate Niku output."""
|
|
429
|
+
# To be implemented
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
def _generate_tjp(self) -> None:
|
|
433
|
+
"""Generate TJP export output."""
|
|
434
|
+
# To be implemented
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
def _generate_msp_xml(self) -> None:
|
|
438
|
+
"""Generate MS Project XML output."""
|
|
439
|
+
# To be implemented
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
def _copy_auxiliary_files(self) -> None:
|
|
443
|
+
"""Copy CSS and other auxiliary files to output directory."""
|
|
444
|
+
# To be implemented - copy CSS files, icons, etc.
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
def addReport(self, report: 'Report') -> None:
|
|
448
|
+
"""
|
|
449
|
+
Add this report to the project.
|
|
450
|
+
This is called from __init__ to register with project.
|
|
451
|
+
|
|
452
|
+
Note: This method is on Project, not Report. It's here for documentation.
|
|
453
|
+
"""
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def add_report_to_project(project: 'Project', report: Report) -> None:
|
|
458
|
+
"""
|
|
459
|
+
Helper function to add a report to a project.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
project: The project to add the report to
|
|
463
|
+
report: The report to add
|
|
464
|
+
"""
|
|
465
|
+
# The report is already added via PropertySet in __init__
|
|
466
|
+
pass
|