ixbrl-viewer 1.4.14__py3-none-any.whl → 1.4.16__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.

Files changed (44) hide show
  1. iXBRLViewerPlugin/__init__.py +87 -66
  2. iXBRLViewerPlugin/_version.py +2 -2
  3. iXBRLViewerPlugin/constants.py +6 -0
  4. iXBRLViewerPlugin/iXBRLViewer.py +36 -32
  5. iXBRLViewerPlugin/ui.py +16 -3
  6. iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
  7. iXBRLViewerPlugin/viewer/src/html/inspector.html +27 -0
  8. iXBRLViewerPlugin/viewer/src/i18n/en/translation.json +18 -1
  9. iXBRLViewerPlugin/viewer/src/i18n/es/translation.json +18 -1
  10. iXBRLViewerPlugin/viewer/src/icons/calculator.svg +13 -0
  11. iXBRLViewerPlugin/viewer/src/icons/circle-cross.svg +11 -0
  12. iXBRLViewerPlugin/viewer/src/icons/circle-tick.svg +11 -0
  13. iXBRLViewerPlugin/viewer/src/icons/dimension.svg +1 -5
  14. iXBRLViewerPlugin/viewer/src/icons/member.svg +2 -5
  15. iXBRLViewerPlugin/viewer/src/icons/multi-tag.svg +10 -0
  16. iXBRLViewerPlugin/viewer/src/js/accordian.js +2 -2
  17. iXBRLViewerPlugin/viewer/src/js/calculation.js +202 -0
  18. iXBRLViewerPlugin/viewer/src/js/calculation.test.js +306 -0
  19. iXBRLViewerPlugin/viewer/src/js/calculationInspector.js +190 -0
  20. iXBRLViewerPlugin/viewer/src/js/concept.js +7 -1
  21. iXBRLViewerPlugin/viewer/src/js/fact.js +59 -5
  22. iXBRLViewerPlugin/viewer/src/js/factset.js +54 -5
  23. iXBRLViewerPlugin/viewer/src/js/factset.test.js +71 -5
  24. iXBRLViewerPlugin/viewer/src/js/inspector.js +85 -30
  25. iXBRLViewerPlugin/viewer/src/js/interval.js +70 -0
  26. iXBRLViewerPlugin/viewer/src/js/interval.test.js +153 -0
  27. iXBRLViewerPlugin/viewer/src/js/test-utils.js +19 -0
  28. iXBRLViewerPlugin/viewer/src/less/accordian.less +2 -2
  29. iXBRLViewerPlugin/viewer/src/less/calculation-inspector.less +83 -0
  30. iXBRLViewerPlugin/viewer/src/less/colours.less +2 -0
  31. iXBRLViewerPlugin/viewer/src/less/dialog.less +8 -4
  32. iXBRLViewerPlugin/viewer/src/less/inspector.less +67 -23
  33. iXBRLViewerPlugin/viewer/src/less/validation-report.less +1 -2
  34. iXBRLViewerPlugin/viewer/src/less/viewer.less +1 -1
  35. iXBRLViewerPlugin/xhtmlserialize.py +1 -1
  36. {ixbrl_viewer-1.4.14.dist-info → ixbrl_viewer-1.4.16.dist-info}/METADATA +2 -2
  37. {ixbrl_viewer-1.4.14.dist-info → ixbrl_viewer-1.4.16.dist-info}/RECORD +43 -34
  38. tests/puppeteer/tools/generate.sh +1 -1
  39. iXBRLViewerPlugin/viewer/src/js/calculations.js +0 -111
  40. {ixbrl_viewer-1.4.14.dist-info → ixbrl_viewer-1.4.16.dist-info}/LICENSE +0 -0
  41. {ixbrl_viewer-1.4.14.dist-info → ixbrl_viewer-1.4.16.dist-info}/NOTICE +0 -0
  42. {ixbrl_viewer-1.4.14.dist-info → ixbrl_viewer-1.4.16.dist-info}/WHEEL +0 -0
  43. {ixbrl_viewer-1.4.14.dist-info → ixbrl_viewer-1.4.16.dist-info}/entry_points.txt +0 -0
  44. {ixbrl_viewer-1.4.14.dist-info → ixbrl_viewer-1.4.16.dist-info}/top_level.txt +0 -0
@@ -9,18 +9,20 @@ import sys
9
9
  import tempfile
10
10
  import traceback
11
11
  from optparse import OptionGroup, OptionParser
12
+ from pathlib import Path
12
13
 
13
14
  from arelle import Cntlr
14
15
  from arelle.LocalViewer import LocalViewer
15
16
  from arelle.ModelDocument import Type
17
+ from arelle.UrlUtil import isHttpUrl
16
18
  from arelle.webserver.bottle import static_file
17
19
 
18
- from .constants import CONFIG_FEATURE_PREFIX, CONFIG_LAUNCH_ON_LOAD, \
19
- CONFIG_SCRIPT_URL, DEFAULT_LAUNCH_ON_LOAD, DEFAULT_OUTPUT_NAME, \
20
- DEFAULT_JS_FILENAME, DEFAULT_VIEWER_PATH, FEATURE_CONFIGS
20
+ from .constants import CONFIG_COPY_SCRIPT, CONFIG_FEATURE_PREFIX, CONFIG_LAUNCH_ON_LOAD, \
21
+ CONFIG_SCRIPT_URL, DEFAULT_COPY_SCRIPT, DEFAULT_LAUNCH_ON_LOAD, DEFAULT_OUTPUT_NAME, \
22
+ DEFAULT_JS_FILENAME, DEFAULT_VIEWER_PATH, ERROR_MESSAGE_CODE, \
23
+ EXCEPTION_MESSAGE_CODE, FEATURE_CONFIGS
21
24
  from .iXBRLViewer import IXBRLViewerBuilder, IXBRLViewerBuilderError
22
25
 
23
-
24
26
  #
25
27
  # GUI operation:
26
28
  #
@@ -54,7 +56,18 @@ def iXBRLViewerCommandLineOptionExtender(parser, *args, **kwargs):
54
56
  action="store",
55
57
  dest="viewerURL",
56
58
  default=DEFAULT_VIEWER_PATH,
57
- help="Specify the URL to ixbrlviewer.js")
59
+ help="A filepath or URL to the iXBRL Viewer JavaScript file which will be downloaded or copied into the output directory."
60
+ " If a relative filepath is to be copied it will be resolved relative to the current working directory."
61
+ " If '--viewer-no-copy-script' is used, the '--viewer-url' file will not be copied or downloaded and instead directly referenced from the HTML file."
62
+ " Examples: 'customViewerScript.js', '/path/to/ixbrlviewer.js', 'https://example.com/ixbrlviewer.js'."
63
+ f" The default value is '{DEFAULT_JS_FILENAME}'.")
64
+ parser.add_option("--viewer-no-copy-script",
65
+ action="store_true",
66
+ dest="viewerNoCopyScript",
67
+ default=False,
68
+ help="Prevent copying the iXBRL Viewer's JavaScript file from '--viewer-url' into the output directory."
69
+ " If used, the iXBRL Viewer HTML file will reference the '--viewer-url' directly."
70
+ " It must be a valid script location at the time the viewer is opened in a browser.")
58
71
  parser.add_option("--viewer-validation-messages",
59
72
  dest="validationMessages",
60
73
  action="store_true",
@@ -102,47 +115,63 @@ def generateViewer(
102
115
  zipViewerOutput: bool = False,
103
116
  features: list[str] | None = None,
104
117
  packageDownloadURL: str | None = None,
118
+ copyScript: bool = True,
105
119
  ) -> None:
106
120
  """
107
- Generate and save a viewer at the given destination (file, directory, or in-memory file) with the given viewer URL.
108
- If the viewer URL is a location on the local file system, a copy will be placed included in the output destination.
121
+ Generate and save an iXBRL viewer at the given destination (file, directory, or in-memory file) with the given viewer script URL.
122
+ If copyScript is True the viewer script will be copied into the output destination.
109
123
  :param cntlr: The arelle controller that contains the model to be included in the viewer
110
124
  :param saveViewerDest: The target that viewer data/files will be written to (path to file or directory, or a file object itself).
111
- :param viewerURL: The filepath or URL location of the viewer script.
125
+ :param viewerURL: The filepath or URL location of the iXBRL Viewer JavaScript file.
112
126
  :param showValidationMessages: True if validation messages should be shown in the viewer.
113
127
  :param useStubViewer: True if the stub viewer should be used.
114
128
  :param zipViewerOutput: True if the destination is a zip archive.
115
- :param features: List of feature names to enable via generated JSON data.
129
+ :param features: Optional list of features to enable.
130
+ :param packageDownloadURL: Optional URL to use as the report package download URL.
131
+ :param copyScript: Controls if the script referenced by viewerURL is copied into the output directory, or directly set as the 'src' value of the script tag in the HTML iXBRL Viewer.
116
132
  """
117
133
  # extend XBRL-loaded run processing for this option
118
- if saveViewerDest is None:
134
+ abortGenerationMsg = "Skipping iXBRL Viewer generation."
135
+ if not saveViewerDest:
136
+ cntlr.addToLog(f"iXBRL Viewer destination not provided. {abortGenerationMsg}", messageCode=EXCEPTION_MESSAGE_CODE)
119
137
  return
120
- if (cntlr.modelManager is None
121
- or len(cntlr.modelManager.loadedModelXbrls) == 0
138
+
139
+ if not viewerURL:
140
+ cntlr.addToLog(f"iXBRL Viewer script not provided. {abortGenerationMsg}", messageCode=EXCEPTION_MESSAGE_CODE)
141
+ return
142
+
143
+ if (cntlr.modelManager is None
144
+ or len(cntlr.modelManager.loadedModelXbrls) == 0
122
145
  or any(not mx.modelDocument for mx in cntlr.modelManager.loadedModelXbrls)):
123
- cntlr.addToLog("No taxonomy loaded.")
146
+ cntlr.addToLog(f"No taxonomy loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
124
147
  return
125
- modelXbrl = cntlr.modelManager.modelXbrl
126
- if modelXbrl.modelDocument.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
127
- cntlr.addToLog("No inline XBRL document loaded.")
148
+ if cntlr.modelManager.modelXbrl.modelDocument.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
149
+ cntlr.addToLog(f"No inline XBRL document loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
128
150
  return
151
+
129
152
  copyScriptPath = None
130
- if isinstance(saveViewerDest, str):
131
- # Note on URLs: Rather than rely on logic to determine if the input is a file
132
- # path or web address, we can allow web addresses to be considered relative paths.
133
- # Unless the URL happens to resolve to an existing file on the local filesystem,
134
- # it will skip this step and pass through into the viewer as expected.
135
- if os.path.isabs(viewerURL):
136
- viewerAbsolutePath = viewerURL
137
- else:
138
- viewerAbsolutePath = getAbsoluteViewerPath(saveViewerDest, viewerURL)
139
-
140
- if os.path.isfile(viewerAbsolutePath):
141
- # The script was found on the local file system and will be copied into the
142
- # destination directory, so the local path (just the basename) of viewerURL should
143
- # be passed to the script tag
144
- copyScriptPath = viewerURL
145
- viewerURL = os.path.basename(viewerURL)
153
+ if copyScript:
154
+ originalViewerURL = viewerURL
155
+ viewerPath = None
156
+ if isHttpUrl(originalViewerURL):
157
+ cacheScript = cntlr.webCache.getfilename(originalViewerURL)
158
+ if cacheScript and (cacheScriptPath := Path(cacheScript)).is_file():
159
+ viewerPath = cacheScriptPath
160
+ else:
161
+ downloadFailedErrorMessage = f"Unable to download iXBRL Viewer script '{originalViewerURL}'."
162
+ if cntlr.webCache.workOffline:
163
+ downloadFailedErrorMessage += " Disable offline mode and try again."
164
+ cntlr.addToLog(f"{downloadFailedErrorMessage} {abortGenerationMsg}", messageCode=EXCEPTION_MESSAGE_CODE)
165
+ return
166
+
167
+ if not viewerPath:
168
+ viewerPath = Path(viewerURL)
169
+ copyScriptPath = viewerPath.resolve()
170
+ viewerURL = viewerPath.name
171
+ if not viewerPath.is_file():
172
+ cntlr.addToLog(f"iXBRL Viewer script not found at '{viewerPath}'. {abortGenerationMsg}", messageCode=EXCEPTION_MESSAGE_CODE)
173
+ return
174
+
146
175
  try:
147
176
  viewerBuilder = IXBRLViewerBuilder(cntlr.modelManager.loadedModelXbrls)
148
177
  if features:
@@ -154,20 +183,8 @@ def generateViewer(
154
183
  except IXBRLViewerBuilderError as ex:
155
184
  print(ex)
156
185
  except Exception as ex:
157
- cntlr.addToLog("Exception {} \nTraceback {}".format(ex, traceback.format_tb(sys.exc_info()[2])))
158
-
159
-
160
- def getAbsoluteViewerPath(saveViewerPath: str, relativeViewerPath: str) -> str:
161
- """
162
- Generate a path to the viewer script given the save destination path as a starting point.
163
- :param saveViewerPath: Path to file or directory where viewer output will be saved.
164
- :param relativeViewerPath: Path to save destination relative to viewer save path.
165
- :return: An absolute file path to the viewer.
166
- """
167
- saveViewerDir = saveViewerPath
168
- if os.path.isfile(saveViewerDir):
169
- saveViewerDir = os.path.dirname(os.path.join(os.getcwd(), saveViewerDir))
170
- return os.path.join(saveViewerDir, relativeViewerPath)
186
+ tb = traceback.format_tb(sys.exc_info()[2])
187
+ cntlr.addToLog(f"Exception {ex} \nTraceback {tb}", messageCode=EXCEPTION_MESSAGE_CODE)
171
188
 
172
189
 
173
190
  def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
@@ -180,35 +197,37 @@ def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
180
197
 
181
198
  def iXBRLViewerCommandLineXbrlRun(cntlr, options, *args, **kwargs):
182
199
  generateViewer(
183
- cntlr,
184
- options.saveViewerDest or kwargs.get("responseZipStream"),
185
- options.viewerURL,
186
- options.validationMessages,
187
- options.useStubViewer,
188
- options.zipViewerOutput,
189
- getFeaturesFromOptions(options),
190
- options.packageDownloadURL,
200
+ cntlr=cntlr,
201
+ saveViewerDest=options.saveViewerDest or kwargs.get("responseZipStream"),
202
+ viewerURL=options.viewerURL,
203
+ copyScript=not options.viewerNoCopyScript,
204
+ showValidationMessages=options.validationMessages,
205
+ useStubViewer=options.useStubViewer,
206
+ zipViewerOutput=options.zipViewerOutput,
207
+ features=getFeaturesFromOptions(options),
208
+ packageDownloadURL=options.packageDownloadURL,
191
209
  )
192
210
 
193
211
 
194
212
  def iXBRLViewerSaveCommand(cntlr):
195
213
  from .ui import SaveViewerDialog
196
214
  if cntlr.modelManager is None or cntlr.modelManager.modelXbrl is None:
197
- cntlr.addToLog("No document loaded.")
215
+ cntlr.addToLog("No document loaded.", messageCode=ERROR_MESSAGE_CODE)
198
216
  return
199
217
  modelXbrl = cntlr.modelManager.modelXbrl
200
218
  if modelXbrl.modelDocument.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
201
- cntlr.addToLog("No inline XBRL document loaded.")
219
+ cntlr.addToLog("No inline XBRL document loaded.", messageCode=ERROR_MESSAGE_CODE)
202
220
  return
203
221
  dialog = SaveViewerDialog(cntlr)
204
222
  dialog.render()
205
223
  if dialog.accepted and dialog.filename():
206
224
  generateViewer(
207
- cntlr,
208
- dialog.filename(),
209
- dialog.scriptUrl() or DEFAULT_VIEWER_PATH,
225
+ cntlr=cntlr,
226
+ saveViewerDest=dialog.filename(),
227
+ viewerURL=dialog.scriptUrl() or DEFAULT_VIEWER_PATH,
228
+ copyScript=dialog.copyScript(),
210
229
  zipViewerOutput=dialog.zipViewerOutput(),
211
- features=dialog.features()
230
+ features=dialog.features(),
212
231
  )
213
232
 
214
233
 
@@ -284,18 +303,20 @@ def guiRun(cntlr, modelXbrl, attach, *args, **kwargs):
284
303
  if cntlr.config.setdefault(f'{CONFIG_FEATURE_PREFIX}{c.key}', False)
285
304
  ]
286
305
  generateViewer(
287
- cntlr,
306
+ cntlr=cntlr,
288
307
  saveViewerDest=tempViewer.name,
289
308
  viewerURL=cntlr.config.get(CONFIG_SCRIPT_URL) or DEFAULT_VIEWER_PATH,
309
+ copyScript=cntlr.config.get(CONFIG_COPY_SCRIPT, DEFAULT_COPY_SCRIPT),
290
310
  useStubViewer=True,
291
- features=features
311
+ features=features,
292
312
  )
293
- localViewer = iXBRLViewerLocalViewer("iXBRL Viewer", os.path.dirname(__file__))
294
- localhost = localViewer.init(cntlr, tempViewer.name)
295
- webbrowser.open(f'{localhost}/{viewer_file_name}')
313
+ if Path(tempViewer.name, viewer_file_name).exists():
314
+ localViewer = iXBRLViewerLocalViewer("iXBRL Viewer", os.path.dirname(__file__))
315
+ localhost = localViewer.init(cntlr, tempViewer.name)
316
+ webbrowser.open(f'{localhost}/{viewer_file_name}')
296
317
  except Exception as ex:
297
318
  modelXbrl.error(
298
- "viewer:exception",
319
+ EXCEPTION_MESSAGE_CODE,
299
320
  "Exception %(exception)s \nTraceback %(traceback)s",
300
321
  modelObject=modelXbrl, exception=ex, traceback=traceback.format_tb(sys.exc_info()[2])
301
322
  )
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.4.14'
16
- __version_tuple__ = version_tuple = (1, 4, 14)
15
+ __version__ = version = '1.4.16'
16
+ __version_tuple__ = version_tuple = (1, 4, 16)
@@ -7,11 +7,17 @@ from .featureConfig import FeatureConfig
7
7
  CONFIG_FEATURE_PREFIX = 'iXBRLViewerFeature_'
8
8
  CONFIG_FILE_DIRECTORY = 'iXBRLViewerFileDir'
9
9
  CONFIG_LAUNCH_ON_LOAD = 'iXBRLViewerLaunchOnLoad'
10
+ CONFIG_COPY_SCRIPT = 'iXBRLViewerCopyScript'
10
11
  CONFIG_OUTPUT_FILE = 'iXBRLViewerOutputFile'
11
12
  CONFIG_SCRIPT_URL = 'iXBRLViewerScriptURL'
12
13
  CONFIG_ZIP_OUTPUT = 'iXBRLViewerZipOutput'
13
14
 
15
+ ERROR_MESSAGE_CODE = 'viewer:error'
16
+ EXCEPTION_MESSAGE_CODE = 'viewer:exception'
17
+ INFO_MESSAGE_CODE = 'viewer:info'
18
+
14
19
  DEFAULT_LAUNCH_ON_LOAD = True
20
+ DEFAULT_COPY_SCRIPT = True
15
21
  DEFAULT_OUTPUT_NAME = 'ixbrlviewer.html'
16
22
  DEFAULT_JS_FILENAME = 'ixbrlviewer.js'
17
23
  DEFAULT_VIEWER_PATH = os.path.join(os.path.dirname(__file__), "viewer", "dist", DEFAULT_JS_FILENAME)
@@ -12,6 +12,7 @@ import urllib.parse
12
12
  import zipfile
13
13
  from collections import defaultdict
14
14
  from copy import deepcopy
15
+ from pathlib import Path
15
16
 
16
17
  import pycountry
17
18
  from arelle import XbrlConst
@@ -23,7 +24,7 @@ from arelle.UrlUtil import isHttpUrl
23
24
  from arelle.ValidateXbrlCalcs import inferredDecimals
24
25
  from lxml import etree
25
26
 
26
- from .constants import DEFAULT_OUTPUT_NAME, DEFAULT_VIEWER_PATH, FEATURE_CONFIGS
27
+ from .constants import DEFAULT_JS_FILENAME, DEFAULT_OUTPUT_NAME, ERROR_MESSAGE_CODE, FEATURE_CONFIGS, INFO_MESSAGE_CODE
27
28
  from .xhtmlserialize import XHTMLSerializer
28
29
 
29
30
 
@@ -265,11 +266,15 @@ class IXBRLViewerBuilder:
265
266
  factData["v"] = None
266
267
  elif f.concept is not None and f.concept.isEnumeration:
267
268
  qnEnums = f.xValue
268
- if not isinstance(qnEnums, list):
269
- qnEnums = (qnEnums,)
270
- factData["v"] = " ".join(self.nsmap.qname(qn) for qn in qnEnums)
271
- for qn in qnEnums:
272
- self.addConcept(report, report.qnameConcepts.get(qn))
269
+ if qnEnums is None:
270
+ factData["v"] = f.value
271
+ factData["err"] = 'INVALID_IX_VALUE'
272
+ else:
273
+ if not isinstance(qnEnums, list):
274
+ qnEnums = (qnEnums,)
275
+ factData["v"] = " ".join(self.nsmap.qname(qn) for qn in qnEnums)
276
+ for qn in qnEnums:
277
+ self.addConcept(report, report.qnameConcepts.get(qn))
273
278
  else:
274
279
  factData["v"] = f.value
275
280
  if f.value == INVALIDixVALUE:
@@ -348,7 +353,7 @@ class IXBRLViewerBuilder:
348
353
  if child.tag == '{http://www.w3.org/1999/xhtml}body':
349
354
  for body_child in child:
350
355
  if body_child.tag == '{http://www.w3.org/1999/xhtml}script' and body_child.get('type','') == 'application/x.ixbrl-viewer+json':
351
- self.logger_model.error("viewer:error", "File already contains iXBRL viewer")
356
+ self.logger_model.error(ERROR_MESSAGE_CODE, "File already contains iXBRL viewer")
352
357
  return False
353
358
 
354
359
  child.append(etree.Comment("BEGIN IXBRL VIEWER EXTENSIONS"))
@@ -391,7 +396,7 @@ class IXBRLViewerBuilder:
391
396
 
392
397
  def createViewer(
393
398
  self,
394
- scriptUrl: str = DEFAULT_VIEWER_PATH,
399
+ scriptUrl: str = DEFAULT_JS_FILENAME,
395
400
  useStubViewer: bool = False,
396
401
  showValidations: bool = True,
397
402
  packageDownloadURL: str | None = None,
@@ -426,7 +431,7 @@ class IXBRLViewerBuilder:
426
431
  self.currentTargetReport["rels"] = self.getRelationships(report)
427
432
 
428
433
  docSetFiles = None
429
- report.info("viewer:info", "Creating iXBRL viewer (%d of %d)" % (n+1, len(self.reports)))
434
+ report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d of %d)" % (n+1, len(self.reports)))
430
435
  if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
431
436
  # Sort by object index to preserve order in which files were specified.
432
437
  xmlDocsByFilename = {
@@ -531,7 +536,7 @@ class iXBRLViewer:
531
536
  def addFilingDoc(self, filingDocuments):
532
537
  self.filingDocuments = filingDocuments
533
538
 
534
- def save(self, destination: io.BytesIO | str, zipOutput: bool = False, copyScriptPath: str | None = None):
539
+ def save(self, destination: io.BytesIO | str, zipOutput: bool = False, copyScriptPath: Path | None = None):
535
540
  """
536
541
  Save the iXBRL viewer.
537
542
  :param destination: The target that viewer data/files will be written to (path to file/directory, or a file object itself).
@@ -549,15 +554,15 @@ class iXBRLViewer:
549
554
  fileMode = 'w'
550
555
  elif destination.endswith(os.sep):
551
556
  # Looks like a directory, but isn't one
552
- self.logger_model.error("viewer:error", "Directory %s does not exist" % destination)
557
+ self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
553
558
  return
554
559
  elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
555
560
  # Directory part of filename doesn't exist
556
- self.logger_model.error("viewer:error", "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
561
+ self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
557
562
  return
558
563
  elif not destination.endswith('.zip'):
559
564
  # File extension isn't a zip
560
- self.logger_model.error("viewer:error", "File extension %s is not a zip" % os.path.splitext(destination)[0])
565
+ self.logger_model.error(ERROR_MESSAGE_CODE, "File extension %s is not a zip" % os.path.splitext(destination)[0])
561
566
  return
562
567
  else:
563
568
  file = destination
@@ -565,57 +570,56 @@ class iXBRLViewer:
565
570
 
566
571
  with zipfile.ZipFile(file, fileMode, zipfile.ZIP_DEFLATED, True) as zout:
567
572
  for f in self.files:
568
- self.logger_model.info("viewer:info", "Saving in output zip %s" % f.filename)
573
+ self.logger_model.info(INFO_MESSAGE_CODE, "Saving in output zip %s" % f.filename)
569
574
  with zout.open(f.filename, "w") as fout:
570
575
  writer = XHTMLSerializer(fout)
571
576
  writer.serialize(f.xmlDocument)
572
577
  if self.filingDocuments:
573
578
  filename = os.path.basename(self.filingDocuments)
574
- self.logger_model.info("viewer:info", "Writing %s" % filename)
579
+ self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
575
580
  zout.write(self.filingDocuments, filename)
576
581
  if copyScriptPath is not None:
577
- scriptSrc = os.path.join(destination, copyScriptPath)
578
- self.logger_model.info("viewer:info", "Writing script from %s" % scriptSrc)
579
- zout.write(scriptSrc, os.path.basename(copyScriptPath))
582
+ self.logger_model.info(INFO_MESSAGE_CODE, f"Writing script from {copyScriptPath}")
583
+ zout.write(copyScriptPath, copyScriptPath.name)
580
584
  elif os.path.isdir(destination):
581
585
  # If output is a directory, write each file in the doc set to that
582
586
  # directory using its existing filename
583
587
  for f in self.files:
584
588
  filename = os.path.join(destination, f.filename)
585
- self.logger_model.info("viewer:info", "Writing %s" % filename)
589
+ self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
586
590
  with open(filename, "wb") as fout:
587
591
  writer = XHTMLSerializer(fout)
588
592
  writer.serialize(f.xmlDocument)
589
593
  if self.filingDocuments:
590
594
  filename = os.path.basename(self.filingDocuments)
591
- self.logger_model.info("viewer:info", "Writing %s" % filename)
595
+ self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
592
596
  shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
593
597
  if copyScriptPath is not None:
594
- self._copyScript(destination, copyScriptPath)
598
+ self._copyScript(Path(destination), copyScriptPath)
595
599
  else:
596
600
  if len(self.files) > 1:
597
- self.logger_model.error("viewer:error", "More than one file in input, but output is not a directory")
601
+ self.logger_model.error(ERROR_MESSAGE_CODE, "More than one file in input, but output is not a directory")
598
602
  elif destination.endswith(os.sep):
599
603
  # Looks like a directory, but isn't one
600
- self.logger_model.error("viewer:error", "Directory %s does not exist" % destination)
604
+ self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
601
605
  elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
602
606
  # Directory part of filename doesn't exist
603
- self.logger_model.error("viewer:error", "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
607
+ self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
604
608
  else:
605
- self.logger_model.info("viewer:info", "Writing %s" % destination)
609
+ self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % destination)
606
610
  with open(destination, "wb") as fout:
607
611
  writer = XHTMLSerializer(fout)
608
612
  writer.serialize(self.files[0].xmlDocument)
609
613
  if self.filingDocuments:
610
614
  filename = os.path.basename(self.filingDocuments)
611
- self.logger_model.info("viewer:info", "Writing %s" % filename)
615
+ self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
612
616
  shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
613
617
  if copyScriptPath is not None:
614
- outDirectory = os.path.dirname(os.path.join(os.getcwd(), destination))
618
+ outDirectory = Path(destination).parent
615
619
  self._copyScript(outDirectory, copyScriptPath)
616
620
 
617
- def _copyScript(self, directory: str, scriptPath: str):
618
- scriptSrc = os.path.join(directory, scriptPath)
619
- scriptDest = os.path.join(directory, os.path.basename(scriptPath))
620
- self.logger_model.info("viewer:info", "Copying script from %s to %s" % (scriptSrc, scriptDest))
621
- shutil.copy2(scriptSrc, scriptDest)
621
+ def _copyScript(self, destDirectory: Path, scriptPath: Path):
622
+ scriptDest = destDirectory / scriptPath.name
623
+ if scriptPath != scriptDest:
624
+ self.logger_model.info(INFO_MESSAGE_CODE, f"Copying script from {scriptDest} to {scriptDest}.")
625
+ shutil.copy2(scriptPath, scriptDest)
iXBRLViewerPlugin/ui.py CHANGED
@@ -8,9 +8,9 @@ except ImportError:
8
8
 
9
9
  import os
10
10
 
11
- from .constants import CONFIG_FEATURE_PREFIX, CONFIG_FILE_DIRECTORY, CONFIG_LAUNCH_ON_LOAD, \
12
- CONFIG_OUTPUT_FILE, CONFIG_SCRIPT_URL, CONFIG_ZIP_OUTPUT, DEFAULT_LAUNCH_ON_LOAD, \
13
- GUI_FEATURE_CONFIGS
11
+ from .constants import CONFIG_COPY_SCRIPT, CONFIG_FEATURE_PREFIX, CONFIG_FILE_DIRECTORY, \
12
+ CONFIG_LAUNCH_ON_LOAD, CONFIG_OUTPUT_FILE, CONFIG_SCRIPT_URL, CONFIG_ZIP_OUTPUT, \
13
+ DEFAULT_COPY_SCRIPT, DEFAULT_LAUNCH_ON_LOAD, GUI_FEATURE_CONFIGS
14
14
 
15
15
  UNSET_SCRIPT_URL = ''
16
16
 
@@ -30,6 +30,8 @@ class BaseViewerDialog(Toplevel):
30
30
  self._features[featureConfig.key] = featureVar
31
31
  self._scriptUrl = StringVar()
32
32
  self._scriptUrl.set(self.cntlr.config.setdefault(CONFIG_SCRIPT_URL, UNSET_SCRIPT_URL))
33
+ self._copyScript = BooleanVar()
34
+ self._copyScript.set(self.cntlr.config.setdefault(CONFIG_COPY_SCRIPT, DEFAULT_COPY_SCRIPT))
33
35
 
34
36
  def addButtons(self, frame: Frame, x: int, y: int) -> int:
35
37
  """
@@ -62,6 +64,12 @@ class BaseViewerDialog(Toplevel):
62
64
  scriptUrlLabel.grid(row=y, column=0, sticky=W, pady=3, padx=3)
63
65
  scriptUrlEntry.grid(row=y, column=1, columnspan=2, sticky=EW, pady=3, padx=3)
64
66
 
67
+ y += 1
68
+ copyScriptCheckbutton = Checkbutton(frame, text="Copy Script", variable=self._copyScript, onvalue=True, offvalue=False)
69
+ copyScriptLabel = Label(frame, text="Copy the iXBRL Viewer script into the output directory.")
70
+ copyScriptCheckbutton.grid(row=y, column=0, pady=3, padx=3, sticky=W)
71
+ copyScriptLabel.grid(row=y, column=1, columnspan=3, pady=3, padx=3, sticky=W)
72
+
65
73
  y += 1
66
74
  featuresLabel = Label(frame, text="Generate with optional features:")
67
75
  featuresLabel.grid(row=y, column=0, columnspan=2, pady=3, padx=3, sticky=W)
@@ -130,6 +138,9 @@ class BaseViewerDialog(Toplevel):
130
138
  self.grab_set()
131
139
  self.wait_window(self)
132
140
 
141
+ def copyScript(self):
142
+ return self._copyScript.get()
143
+
133
144
  def features(self):
134
145
  # Return list of feature keys with corresponding BooleanVar is set to True
135
146
  return [feature for feature, value in self._features.items() if value.get()]
@@ -238,6 +249,7 @@ class SettingsDialog(BaseViewerDialog):
238
249
  """
239
250
  self.cntlr.config[CONFIG_LAUNCH_ON_LOAD] = self._launchOnLoad.get()
240
251
  self.cntlr.config[CONFIG_SCRIPT_URL] = self._scriptUrl.get()
252
+ self.cntlr.config[CONFIG_COPY_SCRIPT] = self._copyScript.get()
241
253
  for key, var in self._features.items():
242
254
  self.cntlr.config[f'{CONFIG_FEATURE_PREFIX}{key}'] = var.get()
243
255
  self.cntlr.saveConfig()
@@ -249,5 +261,6 @@ class SettingsDialog(BaseViewerDialog):
249
261
  """
250
262
  self._launchOnLoad.set(DEFAULT_LAUNCH_ON_LOAD)
251
263
  self._scriptUrl.set(UNSET_SCRIPT_URL)
264
+ self._copyScript.set(DEFAULT_COPY_SCRIPT)
252
265
  for featureConfig in GUI_FEATURE_CONFIGS:
253
266
  self._features[featureConfig.key].set(featureConfig.guiDefault)