ixbrl-viewer 1.4.28__py3-none-any.whl → 1.4.30__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.
Potentially problematic release.
This version of ixbrl-viewer might be problematic. Click here for more details.
- iXBRLViewerPlugin/__init__.py +43 -22
- iXBRLViewerPlugin/_version.py +2 -2
- iXBRLViewerPlugin/iXBRLViewer.py +146 -125
- iXBRLViewerPlugin/plugin.py +7 -0
- iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
- iXBRLViewerPlugin/viewer/src/i18n/en/translation.json +1 -0
- iXBRLViewerPlugin/viewer/src/js/calculation.js +45 -32
- iXBRLViewerPlugin/viewer/src/js/inspector.js +6 -1
- iXBRLViewerPlugin/viewer/src/js/util.js +3 -0
- {ixbrl_viewer-1.4.28.dist-info → ixbrl_viewer-1.4.30.dist-info}/METADATA +5 -5
- {ixbrl_viewer-1.4.28.dist-info → ixbrl_viewer-1.4.30.dist-info}/RECORD +17 -16
- tests/unit_tests/iXBRLViewerPlugin/test_iXBRLViewer.py +24 -23
- {ixbrl_viewer-1.4.28.dist-info → ixbrl_viewer-1.4.30.dist-info}/LICENSE +0 -0
- {ixbrl_viewer-1.4.28.dist-info → ixbrl_viewer-1.4.30.dist-info}/NOTICE +0 -0
- {ixbrl_viewer-1.4.28.dist-info → ixbrl_viewer-1.4.30.dist-info}/WHEEL +0 -0
- {ixbrl_viewer-1.4.28.dist-info → ixbrl_viewer-1.4.30.dist-info}/entry_points.txt +0 -0
- {ixbrl_viewer-1.4.28.dist-info → ixbrl_viewer-1.4.30.dist-info}/top_level.txt +0 -0
iXBRLViewerPlugin/__init__.py
CHANGED
|
@@ -22,6 +22,9 @@ from .constants import CONFIG_COPY_SCRIPT, CONFIG_FEATURE_PREFIX, CONFIG_LAUNCH_
|
|
|
22
22
|
DEFAULT_JS_FILENAME, DEFAULT_VIEWER_PATH, ERROR_MESSAGE_CODE, \
|
|
23
23
|
EXCEPTION_MESSAGE_CODE, FEATURE_CONFIGS
|
|
24
24
|
from .iXBRLViewer import IXBRLViewerBuilder, IXBRLViewerBuilderError
|
|
25
|
+
from .plugin import IXBRLViewerPluginData
|
|
26
|
+
|
|
27
|
+
PLUGIN_NAME = 'ixbrl-viewer'
|
|
25
28
|
|
|
26
29
|
#
|
|
27
30
|
# GUI operation:
|
|
@@ -94,10 +97,6 @@ def iXBRLViewerCommandLineOptionExtender(parser, *args, **kwargs):
|
|
|
94
97
|
dest="zipViewerOutput",
|
|
95
98
|
help="Converts the viewer output into a self contained zip")
|
|
96
99
|
|
|
97
|
-
# Force "keepOpen" to true, so that all models are retained. Needed for
|
|
98
|
-
# multi-instance viewers.
|
|
99
|
-
parser.set_defaults(keepOpen = True)
|
|
100
|
-
|
|
101
100
|
featureGroup = OptionGroup(parser, "Viewer Features",
|
|
102
101
|
"See viewer README for information on enabling/disabling features.")
|
|
103
102
|
for featureConfig in FEATURE_CONFIGS:
|
|
@@ -105,13 +104,30 @@ def iXBRLViewerCommandLineOptionExtender(parser, *args, **kwargs):
|
|
|
105
104
|
featureGroup.add_option(arg, arg.lower(), action="store_true", default=False, help=featureConfig.description)
|
|
106
105
|
parser.add_option_group(featureGroup)
|
|
107
106
|
|
|
107
|
+
def pluginData(cntlr: Cntlr):
|
|
108
|
+
pluginData = cntlr.getPluginData(PLUGIN_NAME)
|
|
109
|
+
if pluginData is None:
|
|
110
|
+
pluginData = IXBRLViewerPluginData(PLUGIN_NAME)
|
|
111
|
+
cntlr.setPluginData(pluginData)
|
|
112
|
+
return pluginData
|
|
113
|
+
|
|
114
|
+
def resetPluginData(cntlr: Cntlr):
|
|
115
|
+
pluginData(cntlr).builder = None
|
|
116
|
+
|
|
117
|
+
def processModel(cntlr: Cntlr, modelXbrl: ModelXbrl):
|
|
118
|
+
try:
|
|
119
|
+
pluginData(cntlr).builder.processModel(modelXbrl)
|
|
120
|
+
except IXBRLViewerBuilderError as ex:
|
|
121
|
+
print(ex)
|
|
122
|
+
except Exception as ex:
|
|
123
|
+
tb = traceback.format_tb(sys.exc_info()[2])
|
|
124
|
+
cntlr.addToLog(f"Exception {ex} \nTraceback {tb}", messageCode=EXCEPTION_MESSAGE_CODE)
|
|
108
125
|
|
|
109
126
|
def generateViewer(
|
|
110
127
|
cntlr: Cntlr,
|
|
111
128
|
saveViewerDest: io.BytesIO | str | None,
|
|
112
129
|
viewerURL: str | None = None,
|
|
113
130
|
showValidationMessages: bool = False,
|
|
114
|
-
useStubViewer: bool = False,
|
|
115
131
|
zipViewerOutput: bool = False,
|
|
116
132
|
features: list[str] | None = None,
|
|
117
133
|
packageDownloadURL: str | None = None,
|
|
@@ -138,13 +154,10 @@ def generateViewer(
|
|
|
138
154
|
|
|
139
155
|
viewerURL = viewerURL or DEFAULT_VIEWER_PATH
|
|
140
156
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
cntlr.addToLog(f"No
|
|
145
|
-
return
|
|
146
|
-
if cntlr.modelManager.modelXbrl.modelDocument.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
|
|
147
|
-
cntlr.addToLog(f"No inline XBRL document loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
|
|
157
|
+
bldr = pluginData(cntlr).builder
|
|
158
|
+
|
|
159
|
+
if bldr.reportCount == 0:
|
|
160
|
+
cntlr.addToLog(f"No inline XBRL documents loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
|
|
148
161
|
return
|
|
149
162
|
|
|
150
163
|
copyScriptPath = None
|
|
@@ -171,11 +184,10 @@ def generateViewer(
|
|
|
171
184
|
return
|
|
172
185
|
|
|
173
186
|
try:
|
|
174
|
-
viewerBuilder = IXBRLViewerBuilder(cntlr.modelManager.loadedModelXbrls)
|
|
175
187
|
if features:
|
|
176
188
|
for feature in features:
|
|
177
|
-
|
|
178
|
-
iv =
|
|
189
|
+
bldr.enableFeature(feature)
|
|
190
|
+
iv = bldr.createViewer(scriptUrl=viewerURL, showValidations=showValidationMessages, packageDownloadURL=packageDownloadURL)
|
|
179
191
|
if iv is not None:
|
|
180
192
|
iv.save(saveViewerDest, zipOutput=zipViewerOutput, copyScriptPath=copyScriptPath)
|
|
181
193
|
except IXBRLViewerBuilderError as ex:
|
|
@@ -183,6 +195,7 @@ def generateViewer(
|
|
|
183
195
|
except Exception as ex:
|
|
184
196
|
tb = traceback.format_tb(sys.exc_info()[2])
|
|
185
197
|
cntlr.addToLog(f"Exception {ex} \nTraceback {tb}", messageCode=EXCEPTION_MESSAGE_CODE)
|
|
198
|
+
resetPluginData(cntlr)
|
|
186
199
|
|
|
187
200
|
|
|
188
201
|
def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
|
|
@@ -192,15 +205,19 @@ def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
|
|
|
192
205
|
if getattr(options, f'viewer_feature_{featureConfig.key}') or getattr(options, f'viewer_feature_{featureConfig.key.lower()}')
|
|
193
206
|
]
|
|
194
207
|
|
|
208
|
+
def iXBRLViewerCommandLineXbrlRun(cntlr, options, modelXbrl, *args, **kwargs):
|
|
209
|
+
pd = pluginData(cntlr)
|
|
210
|
+
if pd.builder is None:
|
|
211
|
+
pd.builder = IXBRLViewerBuilder(cntlr, useStubViewer = options.useStubViewer)
|
|
212
|
+
processModel(cntlr, modelXbrl)
|
|
195
213
|
|
|
196
|
-
def
|
|
214
|
+
def iXBRLViewerCommandLineFilingEnd(cntlr, options, *args, **kwargs):
|
|
197
215
|
generateViewer(
|
|
198
216
|
cntlr=cntlr,
|
|
199
217
|
saveViewerDest=options.saveViewerDest or kwargs.get("responseZipStream"),
|
|
200
218
|
viewerURL=options.viewerURL,
|
|
201
219
|
copyScript=not options.viewerNoCopyScript,
|
|
202
220
|
showValidationMessages=options.validationMessages,
|
|
203
|
-
useStubViewer=options.useStubViewer,
|
|
204
221
|
zipViewerOutput=options.zipViewerOutput,
|
|
205
222
|
features=getFeaturesFromOptions(options),
|
|
206
223
|
packageDownloadURL=options.packageDownloadURL,
|
|
@@ -259,9 +276,11 @@ def commandLineOptionExtender(*args, **kwargs):
|
|
|
259
276
|
iXBRLViewerCommandLineOptionExtender(*args, **kwargs)
|
|
260
277
|
|
|
261
278
|
|
|
262
|
-
def commandLineRun(*args, **kwargs):
|
|
263
|
-
iXBRLViewerCommandLineXbrlRun(*args, **kwargs)
|
|
279
|
+
def commandLineRun(cntlr, options, modelXbrl, *args, **kwargs):
|
|
280
|
+
iXBRLViewerCommandLineXbrlRun(cntlr, options, modelXbrl, *args, **kwargs)
|
|
264
281
|
|
|
282
|
+
def commandLineFilingEnd(*args, **kwargs):
|
|
283
|
+
iXBRLViewerCommandLineFilingEnd(*args, **kwargs)
|
|
265
284
|
|
|
266
285
|
class iXBRLViewerLocalViewer(LocalViewer):
|
|
267
286
|
# plugin-specific local file handler
|
|
@@ -300,12 +319,13 @@ def guiRun(cntlr, modelXbrl, attach, *args, **kwargs):
|
|
|
300
319
|
for c in FEATURE_CONFIGS
|
|
301
320
|
if cntlr.config.setdefault(f'{CONFIG_FEATURE_PREFIX}{c.key}', False)
|
|
302
321
|
]
|
|
322
|
+
pluginData(cntlr).builder = IXBRLViewerBuilder(cntlr, useStubViewer = True)
|
|
323
|
+
processModel(cntlr, modelXbrl)
|
|
303
324
|
generateViewer(
|
|
304
325
|
cntlr=cntlr,
|
|
305
326
|
saveViewerDest=tempViewer.name,
|
|
306
327
|
viewerURL=cntlr.config.get(CONFIG_SCRIPT_URL),
|
|
307
328
|
copyScript=cntlr.config.get(CONFIG_COPY_SCRIPT, DEFAULT_COPY_SCRIPT),
|
|
308
|
-
useStubViewer=True,
|
|
309
329
|
features=features,
|
|
310
330
|
)
|
|
311
331
|
if Path(tempViewer.name, viewer_file_name).exists():
|
|
@@ -325,7 +345,7 @@ def load_plugin_url():
|
|
|
325
345
|
|
|
326
346
|
|
|
327
347
|
__pluginInfo__ = {
|
|
328
|
-
'name':
|
|
348
|
+
'name': PLUGIN_NAME,
|
|
329
349
|
'aliases': [
|
|
330
350
|
'iXBRLViewerPlugin',
|
|
331
351
|
],
|
|
@@ -335,7 +355,8 @@ __pluginInfo__ = {
|
|
|
335
355
|
'author': 'Paul Warren',
|
|
336
356
|
'copyright': 'Copyright :: Workiva Inc. :: 2019',
|
|
337
357
|
'CntlrCmdLine.Options': commandLineOptionExtender,
|
|
338
|
-
'CntlrCmdLine.
|
|
358
|
+
'CntlrCmdLine.Xbrl.Run': commandLineRun,
|
|
359
|
+
'CntlrCmdLine.Filing.End': commandLineFilingEnd,
|
|
339
360
|
'CntlrWinMain.Menu.Tools': toolsMenuExtender,
|
|
340
361
|
'CntlrWinMain.Xbrl.Loaded': guiRun,
|
|
341
362
|
}
|
iXBRLViewerPlugin/_version.py
CHANGED
iXBRLViewerPlugin/iXBRLViewer.py
CHANGED
|
@@ -26,7 +26,6 @@ from lxml import etree
|
|
|
26
26
|
from .constants import DEFAULT_JS_FILENAME, DEFAULT_OUTPUT_NAME, ERROR_MESSAGE_CODE, FEATURE_CONFIGS, INFO_MESSAGE_CODE
|
|
27
27
|
from .xhtmlserialize import XHTMLSerializer
|
|
28
28
|
|
|
29
|
-
|
|
30
29
|
UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE = 'unrecognizedLinkbase'
|
|
31
30
|
LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE = {
|
|
32
31
|
XbrlConst.qnLinkCalculationLink: 'calcLinkbase',
|
|
@@ -82,18 +81,38 @@ class IXBRLViewerBuilderError(Exception):
|
|
|
82
81
|
|
|
83
82
|
class IXBRLViewerBuilder:
|
|
84
83
|
|
|
85
|
-
def __init__(self,
|
|
84
|
+
def __init__(self,
|
|
85
|
+
cntlr: Cntlr,
|
|
86
|
+
basenameSuffix: str = '',
|
|
87
|
+
useStubViewer: bool = False,
|
|
88
|
+
):
|
|
86
89
|
self.nsmap = NamespaceMap()
|
|
87
90
|
self.roleMap = NamespaceMap()
|
|
88
|
-
self.reports = reports
|
|
89
|
-
# Arbitrary ModelXbrl used for logging
|
|
90
|
-
self.logger_model = reports[0]
|
|
91
91
|
self.taxonomyData = {
|
|
92
92
|
"sourceReports": [],
|
|
93
93
|
"features": [],
|
|
94
94
|
}
|
|
95
95
|
self.basenameSuffix = basenameSuffix
|
|
96
96
|
self.currentTargetReport = None
|
|
97
|
+
self.useStubViewer = useStubViewer
|
|
98
|
+
self.cntlr = cntlr
|
|
99
|
+
|
|
100
|
+
self.idGen = 0
|
|
101
|
+
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
102
|
+
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
103
|
+
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
104
|
+
self.roleMap.getPrefix(XbrlConst.summationItem, "calc11")
|
|
105
|
+
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
106
|
+
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
107
|
+
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
108
|
+
|
|
109
|
+
self.sourceReportsByFiles = dict()
|
|
110
|
+
self.iv = iXBRLViewer(cntlr)
|
|
111
|
+
if self.useStubViewer:
|
|
112
|
+
self.iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
|
|
113
|
+
|
|
114
|
+
self.fromSingleZIP = None
|
|
115
|
+
self.reportCount = 0
|
|
97
116
|
|
|
98
117
|
def enableFeature(self, featureName: str):
|
|
99
118
|
if featureName in self.taxonomyData["features"]:
|
|
@@ -190,7 +209,7 @@ class IXBRLViewerBuilder:
|
|
|
190
209
|
|
|
191
210
|
for baseSetKey, baseSetModelLinks in report.baseSets.items():
|
|
192
211
|
arcrole, ELR, linkqname, arcqname = baseSetKey
|
|
193
|
-
if arcrole in (XbrlConst.summationItem, WIDER_NARROWER_ARCROLE, XbrlConst.parentChild, XbrlConst.dimensionDefault) and ELR is not None:
|
|
212
|
+
if arcrole in (XbrlConst.summationItem, XbrlConst.summationItem11, WIDER_NARROWER_ARCROLE, XbrlConst.parentChild, XbrlConst.dimensionDefault) and ELR is not None:
|
|
194
213
|
self.addELR(report, ELR)
|
|
195
214
|
rr = dict()
|
|
196
215
|
relSet = report.relationshipSet(arcrole, ELR)
|
|
@@ -210,7 +229,7 @@ class IXBRLViewerBuilder:
|
|
|
210
229
|
return rels
|
|
211
230
|
|
|
212
231
|
def validationErrors(self):
|
|
213
|
-
logHandler = self.
|
|
232
|
+
logHandler = self.cntlr.logHandler
|
|
214
233
|
if getattr(logHandler, "logRecordBuffer") is None:
|
|
215
234
|
raise IXBRLViewerBuilderError("Logging is not configured to use a buffer. Unable to retrieve validation messages")
|
|
216
235
|
|
|
@@ -326,14 +345,13 @@ class IXBRLViewerBuilder:
|
|
|
326
345
|
return numeratorsString
|
|
327
346
|
|
|
328
347
|
def addViewerData(self, viewerFile, scriptUrl):
|
|
329
|
-
viewerFile.xmlDocument = deepcopy(viewerFile.xmlDocument)
|
|
330
348
|
taxonomyDataJSON = self.escapeJSONForScriptTag(json.dumps(self.taxonomyData, indent=1, allow_nan=False))
|
|
331
349
|
|
|
332
350
|
for child in viewerFile.xmlDocument.getroot():
|
|
333
351
|
if child.tag == '{http://www.w3.org/1999/xhtml}body':
|
|
334
352
|
for body_child in child:
|
|
335
353
|
if body_child.tag == '{http://www.w3.org/1999/xhtml}script' and body_child.get('type','') == 'application/x.ixbrl-viewer+json':
|
|
336
|
-
self.
|
|
354
|
+
self.cntlr.addToLog("File already contains iXBRL viewer", messageCode="error")
|
|
337
355
|
return False
|
|
338
356
|
|
|
339
357
|
child.append(etree.Comment("BEGIN IXBRL VIEWER EXTENSIONS"))
|
|
@@ -375,110 +393,103 @@ class IXBRLViewerBuilder:
|
|
|
375
393
|
self.taxonomyData["sourceReports"].append(sourceReport)
|
|
376
394
|
return sourceReport
|
|
377
395
|
|
|
396
|
+
def processModel(
|
|
397
|
+
self,
|
|
398
|
+
report: ModelXbrl
|
|
399
|
+
):
|
|
400
|
+
|
|
401
|
+
self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
|
|
402
|
+
self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
|
|
403
|
+
softwareCredits = set()
|
|
404
|
+
for document in report.urlDocs.values():
|
|
405
|
+
if document.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
|
|
406
|
+
continue
|
|
407
|
+
matches = document.creationSoftwareMatches(document.creationSoftwareComment)
|
|
408
|
+
softwareCredits.update(matches)
|
|
409
|
+
if softwareCredits:
|
|
410
|
+
self.currentTargetReport["softwareCredits"] = list(softwareCredits)
|
|
411
|
+
for f in report.facts:
|
|
412
|
+
self.addFact(report, f)
|
|
413
|
+
self.currentTargetReport["rels"] = self.getRelationships(report)
|
|
414
|
+
|
|
415
|
+
docSetFiles = None
|
|
416
|
+
self.reportCount += 1
|
|
417
|
+
report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d) [%s]" % (self.reportCount, self.currentTargetReport["target"]))
|
|
418
|
+
if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
|
|
419
|
+
# Sort by object index to preserve order in which files were specified.
|
|
420
|
+
xmlDocsByFilename = {
|
|
421
|
+
os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
|
|
422
|
+
for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
|
|
423
|
+
if doc.type == Type.INLINEXBRL
|
|
424
|
+
}
|
|
425
|
+
docSetFiles = list(xmlDocsByFilename.keys())
|
|
426
|
+
|
|
427
|
+
for filename, docSetXMLDoc in xmlDocsByFilename.items():
|
|
428
|
+
self.iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
429
|
+
|
|
430
|
+
elif self.useStubViewer:
|
|
431
|
+
filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
432
|
+
docSetFiles = [ filename ]
|
|
433
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
434
|
+
|
|
435
|
+
else:
|
|
436
|
+
srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
437
|
+
docSetFiles = [ srcFilename ]
|
|
438
|
+
filename = srcFilename
|
|
439
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
440
|
+
|
|
441
|
+
docSetKey = frozenset(docSetFiles)
|
|
442
|
+
sourceReport = self.sourceReportsByFiles.get(docSetKey)
|
|
443
|
+
if sourceReport is None:
|
|
444
|
+
sourceReport = self.addSourceReport()
|
|
445
|
+
self.sourceReportsByFiles[docSetKey] = sourceReport
|
|
446
|
+
sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
|
|
447
|
+
|
|
448
|
+
sourceReport["targetReports"].append(self.currentTargetReport)
|
|
449
|
+
|
|
450
|
+
localDocs = defaultdict(set)
|
|
451
|
+
for path, doc in report.urlDocs.items():
|
|
452
|
+
if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
|
|
453
|
+
continue
|
|
454
|
+
if doc.type == Type.INLINEXBRL:
|
|
455
|
+
localDocs[doc.basename].add('inline')
|
|
456
|
+
elif doc.type == Type.SCHEMA:
|
|
457
|
+
localDocs[doc.basename].add('schema')
|
|
458
|
+
elif doc.type == Type.LINKBASE:
|
|
459
|
+
linkbaseIdentifed = False
|
|
460
|
+
for child in doc.xmlRootElement.iterchildren():
|
|
461
|
+
linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
|
|
462
|
+
if linkbaseLocalDocumentsKey is not None:
|
|
463
|
+
localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
|
|
464
|
+
linkbaseIdentifed = True
|
|
465
|
+
if not linkbaseIdentifed:
|
|
466
|
+
localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
|
|
467
|
+
self.currentTargetReport["localDocs"] = {
|
|
468
|
+
localDoc: sorted(docTypes)
|
|
469
|
+
for localDoc, docTypes in localDocs.items()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# If we only process a single ZIP, add a download link to it as the
|
|
473
|
+
# "filing documents" on the viewer menu.
|
|
474
|
+
if self.fromSingleZIP is None:
|
|
475
|
+
self.fromSingleZIP = report.modelDocument.filepath.endswith(".zip")
|
|
476
|
+
if self.fromSingleZIP:
|
|
477
|
+
self.filingDocZipPath = os.path.dirname(report.modelDocument.filepath)
|
|
478
|
+
else:
|
|
479
|
+
self.fromSingleZIP = False
|
|
480
|
+
|
|
378
481
|
def createViewer(
|
|
379
482
|
self,
|
|
380
483
|
scriptUrl: str = DEFAULT_JS_FILENAME,
|
|
381
|
-
useStubViewer: bool = False,
|
|
382
484
|
showValidations: bool = True,
|
|
383
485
|
packageDownloadURL: str | None = None,
|
|
384
486
|
) -> iXBRLViewer | None:
|
|
385
487
|
"""
|
|
386
488
|
Create an iXBRL file with XBRL data as a JSON blob, and script tags added.
|
|
387
489
|
:param scriptUrl: The `src` value of the script tag that loads the viewer script.
|
|
388
|
-
:param useStubViewer: True if stub document should be included in output.
|
|
389
490
|
:param showValidations: True if validation errors should be included in output taxonomy data.
|
|
390
491
|
:return: An iXBRLViewer instance that is ready to be saved.
|
|
391
492
|
"""
|
|
392
|
-
# This "dts" is only used for logging
|
|
393
|
-
iv = iXBRLViewer(self.reports[0])
|
|
394
|
-
self.idGen = 0
|
|
395
|
-
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
396
|
-
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
397
|
-
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
398
|
-
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
399
|
-
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
400
|
-
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
401
|
-
|
|
402
|
-
sourceReportsByFiles = dict()
|
|
403
|
-
|
|
404
|
-
if useStubViewer:
|
|
405
|
-
iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
|
|
406
|
-
|
|
407
|
-
for n, report in enumerate(self.reports):
|
|
408
|
-
self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
|
|
409
|
-
self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
|
|
410
|
-
softwareCredits = set()
|
|
411
|
-
for document in report.urlDocs.values():
|
|
412
|
-
if document.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
|
|
413
|
-
continue
|
|
414
|
-
matches = document.creationSoftwareMatches(document.creationSoftwareComment)
|
|
415
|
-
softwareCredits.update(matches)
|
|
416
|
-
if softwareCredits:
|
|
417
|
-
self.currentTargetReport["softwareCredits"] = list(softwareCredits)
|
|
418
|
-
for f in report.facts:
|
|
419
|
-
self.addFact(report, f)
|
|
420
|
-
self.currentTargetReport["rels"] = self.getRelationships(report)
|
|
421
|
-
|
|
422
|
-
docSetFiles = None
|
|
423
|
-
report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d of %d)" % (n+1, len(self.reports)))
|
|
424
|
-
if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
|
|
425
|
-
# Sort by object index to preserve order in which files were specified.
|
|
426
|
-
xmlDocsByFilename = {
|
|
427
|
-
os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
|
|
428
|
-
for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
|
|
429
|
-
if doc.type == Type.INLINEXBRL
|
|
430
|
-
}
|
|
431
|
-
docSetFiles = list(xmlDocsByFilename.keys())
|
|
432
|
-
|
|
433
|
-
for filename, docSetXMLDoc in xmlDocsByFilename.items():
|
|
434
|
-
iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
435
|
-
|
|
436
|
-
elif useStubViewer:
|
|
437
|
-
filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
438
|
-
docSetFiles = [ filename ]
|
|
439
|
-
iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
440
|
-
|
|
441
|
-
else:
|
|
442
|
-
srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
443
|
-
docSetFiles = [ srcFilename ]
|
|
444
|
-
if len(self.reports) == 1:
|
|
445
|
-
# If there is only a single report, call the output file "xbrlviewer.html"
|
|
446
|
-
filename = "xbrlviewer.html"
|
|
447
|
-
else:
|
|
448
|
-
# Otherwise, preserve filenames
|
|
449
|
-
filename = srcFilename
|
|
450
|
-
iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
451
|
-
|
|
452
|
-
docSetKey = frozenset(docSetFiles)
|
|
453
|
-
sourceReport = sourceReportsByFiles.get(docSetKey)
|
|
454
|
-
if sourceReport is None:
|
|
455
|
-
sourceReport = self.addSourceReport()
|
|
456
|
-
sourceReportsByFiles[docSetKey] = sourceReport
|
|
457
|
-
sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
|
|
458
|
-
|
|
459
|
-
sourceReport["targetReports"].append(self.currentTargetReport)
|
|
460
|
-
|
|
461
|
-
localDocs = defaultdict(set)
|
|
462
|
-
for path, doc in report.urlDocs.items():
|
|
463
|
-
if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
|
|
464
|
-
continue
|
|
465
|
-
if doc.type == Type.INLINEXBRL:
|
|
466
|
-
localDocs[doc.basename].add('inline')
|
|
467
|
-
elif doc.type == Type.SCHEMA:
|
|
468
|
-
localDocs[doc.basename].add('schema')
|
|
469
|
-
elif doc.type == Type.LINKBASE:
|
|
470
|
-
linkbaseIdentifed = False
|
|
471
|
-
for child in doc.xmlRootElement.iterchildren():
|
|
472
|
-
linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
|
|
473
|
-
if linkbaseLocalDocumentsKey is not None:
|
|
474
|
-
localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
|
|
475
|
-
linkbaseIdentifed = True
|
|
476
|
-
if not linkbaseIdentifed:
|
|
477
|
-
localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
|
|
478
|
-
self.currentTargetReport["localDocs"] = {
|
|
479
|
-
localDoc: sorted(docTypes)
|
|
480
|
-
for localDoc, docTypes in localDocs.items()
|
|
481
|
-
}
|
|
482
493
|
|
|
483
494
|
self.taxonomyData["prefixes"] = self.nsmap.prefixmap
|
|
484
495
|
self.taxonomyData["roles"] = self.roleMap.prefixmap
|
|
@@ -488,32 +499,42 @@ class IXBRLViewerBuilder:
|
|
|
488
499
|
|
|
489
500
|
if packageDownloadURL is not None:
|
|
490
501
|
self.taxonomyData["filingDocuments"] = packageDownloadURL
|
|
491
|
-
elif
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
iv.addFilingDoc(filingDocZipPath)
|
|
502
|
+
elif self.fromSingleZIP:
|
|
503
|
+
filingDocZipName = os.path.basename(self.filingDocZipPath)
|
|
504
|
+
self.iv.addFilingDoc(self.filingDocZipPath)
|
|
495
505
|
self.taxonomyData["filingDocuments"] = filingDocZipName
|
|
496
506
|
|
|
497
|
-
if not self.addViewerData(iv.files[0], scriptUrl):
|
|
507
|
+
if not self.addViewerData(self.iv.files[0], scriptUrl):
|
|
498
508
|
return None
|
|
499
509
|
|
|
500
|
-
|
|
510
|
+
if len(self.iv.files) == 1:
|
|
511
|
+
# If there is only a single report, call the output file "xbrlviewer.html"
|
|
512
|
+
# We should probably preserve the source file extension here.
|
|
513
|
+
self.iv.files[0].filename = 'xbrlviewer.html'
|
|
514
|
+
|
|
515
|
+
return self.iv
|
|
501
516
|
|
|
502
517
|
|
|
503
518
|
class iXBRLViewerFile:
|
|
504
519
|
|
|
505
520
|
def __init__(self, filename, xmlDocument):
|
|
506
521
|
self.filename = filename
|
|
507
|
-
self.xmlDocument = xmlDocument
|
|
522
|
+
self.xmlDocument = deepcopy(xmlDocument)
|
|
523
|
+
# deepcopy does not retain the Python proxies, so iterating the node
|
|
524
|
+
# tree during serialization will create new ones. However, the original
|
|
525
|
+
# ModelObjectFactory is still referenced, and that references a
|
|
526
|
+
# ModelXbrl that will potentially be closed by the time we serialize.
|
|
527
|
+
# Serialization only requires standard XML features, so the default
|
|
528
|
+
# lxml.etree classes (and thus lookup) are fine.
|
|
529
|
+
self.xmlDocument.parser.set_element_class_lookup(etree.ElementDefaultClassLookup())
|
|
508
530
|
|
|
509
531
|
|
|
510
532
|
class iXBRLViewer:
|
|
511
533
|
|
|
512
|
-
def __init__(self,
|
|
534
|
+
def __init__(self, cntlr: Cntlr):
|
|
513
535
|
self.files = []
|
|
514
536
|
self.filingDocuments = None
|
|
515
|
-
|
|
516
|
-
self.logger_model = logger_model
|
|
537
|
+
self.cntlr = cntlr
|
|
517
538
|
self.filenames = set()
|
|
518
539
|
|
|
519
540
|
def addFile(self, ivf):
|
|
@@ -543,15 +564,15 @@ class iXBRLViewer:
|
|
|
543
564
|
fileMode = 'w'
|
|
544
565
|
elif destination.endswith(os.sep):
|
|
545
566
|
# Looks like a directory, but isn't one
|
|
546
|
-
self.
|
|
567
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
547
568
|
return
|
|
548
569
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
549
570
|
# Directory part of filename doesn't exist
|
|
550
|
-
self.
|
|
571
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
551
572
|
return
|
|
552
573
|
elif not destination.endswith('.zip'):
|
|
553
574
|
# File extension isn't a zip
|
|
554
|
-
self.
|
|
575
|
+
self.cntlr.addToLog("File extension %s is not a zip" % os.path.splitext(destination)[0], messageCode=ERROR_MESSAGE_CODE)
|
|
555
576
|
return
|
|
556
577
|
else:
|
|
557
578
|
file = destination
|
|
@@ -559,49 +580,49 @@ class iXBRLViewer:
|
|
|
559
580
|
|
|
560
581
|
with zipfile.ZipFile(file, fileMode, zipfile.ZIP_DEFLATED, True) as zout:
|
|
561
582
|
for f in self.files:
|
|
562
|
-
self.
|
|
583
|
+
self.cntlr.addToLog("Saving in output zip %s" % f.filename, messageCode=INFO_MESSAGE_CODE)
|
|
563
584
|
with zout.open(f.filename, "w") as fout:
|
|
564
585
|
writer = XHTMLSerializer(fout)
|
|
565
586
|
writer.serialize(f.xmlDocument)
|
|
566
587
|
if self.filingDocuments:
|
|
567
588
|
filename = os.path.basename(self.filingDocuments)
|
|
568
|
-
self.
|
|
589
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
569
590
|
zout.write(self.filingDocuments, filename)
|
|
570
591
|
if copyScriptPath is not None:
|
|
571
|
-
self.
|
|
592
|
+
self.cntlr.addToLog(f"Writing script from {copyScriptPath}", messageCode=INFO_MESSAGE_CODE)
|
|
572
593
|
zout.write(copyScriptPath, copyScriptPath.name)
|
|
573
594
|
elif os.path.isdir(destination):
|
|
574
595
|
# If output is a directory, write each file in the doc set to that
|
|
575
596
|
# directory using its existing filename
|
|
576
597
|
for f in self.files:
|
|
577
598
|
filename = os.path.join(destination, f.filename)
|
|
578
|
-
self.
|
|
599
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
579
600
|
with open(filename, "wb") as fout:
|
|
580
601
|
writer = XHTMLSerializer(fout)
|
|
581
602
|
writer.serialize(f.xmlDocument)
|
|
582
603
|
if self.filingDocuments:
|
|
583
604
|
filename = os.path.basename(self.filingDocuments)
|
|
584
|
-
self.
|
|
605
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
585
606
|
shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
|
|
586
607
|
if copyScriptPath is not None:
|
|
587
608
|
self._copyScript(Path(destination), copyScriptPath)
|
|
588
609
|
else:
|
|
589
610
|
if len(self.files) > 1:
|
|
590
|
-
self.
|
|
611
|
+
self.cntlr.addToLog("More than one file in input, but output is not a directory", messageCode=ERROR_MESSAGE_CODE)
|
|
591
612
|
elif destination.endswith(os.sep):
|
|
592
613
|
# Looks like a directory, but isn't one
|
|
593
|
-
self.
|
|
614
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
594
615
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
595
616
|
# Directory part of filename doesn't exist
|
|
596
|
-
self.
|
|
617
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
597
618
|
else:
|
|
598
|
-
self.
|
|
619
|
+
self.cntlr.addToLog("Writing %s" % destination, messageCode=INFO_MESSAGE_CODE)
|
|
599
620
|
with open(destination, "wb") as fout:
|
|
600
621
|
writer = XHTMLSerializer(fout)
|
|
601
622
|
writer.serialize(self.files[0].xmlDocument)
|
|
602
623
|
if self.filingDocuments:
|
|
603
624
|
filename = os.path.basename(self.filingDocuments)
|
|
604
|
-
self.
|
|
625
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
605
626
|
shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
|
|
606
627
|
if copyScriptPath is not None:
|
|
607
628
|
outDirectory = Path(destination).parent
|
|
@@ -610,5 +631,5 @@ class iXBRLViewer:
|
|
|
610
631
|
def _copyScript(self, destDirectory: Path, scriptPath: Path):
|
|
611
632
|
scriptDest = destDirectory / scriptPath.name
|
|
612
633
|
if scriptPath != scriptDest:
|
|
613
|
-
self.
|
|
634
|
+
self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
|
|
614
635
|
shutil.copy2(scriptPath, scriptDest)
|