1#!/usr/bin/env python3
  2
  3"""Represent an HTML report being constructed by a report template."""
  4
  5import os
  6import shutil
  7import logging
  8import collections
  9from io import StringIO
 10from datetime import datetime
 11
 12from django.template.loader import render_to_string
 13
 14from chart.common.path import Path
 15from chart.project import settings
 16from chart.common.exceptions import ConfigError
 17from chart.common import traits
 18from chart.common.prettyprint import Table
 19from chart.common.xml import to_html
 20from chart.common.xml import xml_to_str
 21from chart.common.prettyprint import show_time
 22from chart.products.scos2000.srdb_version_info import get_srdb_version
 23
 24
 25# class Debugstringio(object):
 26#     """Report debugging help.
 27#     This version of StringIO logs it's contents to `logging.debug` as they are entered.
 28#     """
 29
 30#     def __init__(self):
 31#         self.imp = StringIO()
 32
 33#     def write(self, mess):
 34#         """Log a string and write it to the internal string object."""
 35#         logging.debug(mess)
 36#         #super(debugstringio, self).write(m)
 37#         self.imp.write(mess)
 38
 39#     def getvalue(self):
 40#         """Return the internal string object."""
 41#         return self.imp.getvalue()
 42
 43
 44class Document:
 45    """An HTML report."""
 46
 47    def __init__(self):
 48        # name of the starting point for this report i.e. report.html
 49        self.report_filename = None
 50
 51        # content of html title element
 52        self.title = None
 53
 54        # build the final html as a StringIO-like object
 55        self.html = None
 56        self.htmls = None
 57
 58        # document configuration options
 59        self.config = {}
 60
 61        # list of heading text
 62        self.headings = []
 63
 64        # list of subheadings within current heading
 65        self.subheadings = []
 66
 67        # list of figures (titles only)
 68        self.figures = []
 69
 70        # document application specific options
 71        self.globals = {}
 72
 73        # during pre-html pass track current heading number
 74        self.heading_cc = None
 75
 76        # during pre-html pass track subheading number within current heading
 77        self.subheading_cc = None
 78
 79        # current figure number
 80        self.figure_count = None
 81
 82        # list of all generated aux files. These are the only files that get included
 83        # in the final .zip
 84        self.aux_files = []
 85
 86        # Used to generate unique filenames for images. We map widget names or hashes
 87        # against the current count of similar things
 88        self.anon_counts = collections.defaultdict(int)
 89
 90        # list of image filenames used by the LoF widget
 91        self.images = None
 92
 93        # set to the current theme module while we are being instantiated
 94        self.theme = None
 95
 96        # if true, instructs some widgets to dump their underlying data set as a .csv file
 97        # in the report directory
 98        self.dump_values = None
 99
100        # report template object being used to render us
101        self.template = None
102
103    def render_html(self, widgets):
104        """Render each of `widgets` to our `self.html`.
105        This is called by the digest algorithm which captures the output for HTML email.
106        """
107
108        for widget in widgets:
109            logging.info('Processing {widget}'.format(widget=widget))
110            self.html = StringIO()
111            widget.pre_html(self)  # should be removed completely now we have 1-pass rendering
112            try:
113                widget.html(self)
114            except BaseException:
115                logging.error('Error rendering {elem}'.format(elem=xml_to_str(widget.elem)))
116                raise
117
118            self.htmls.append(self.html)
119            self.html = None
120
121        for widget in widgets:
122            widget.post_html(self)
123
124    def render(self, template, debug=False, dump_values=False, sid=None):
125        """Use `template` to instantiate our `html` member variable.
126
127        Args:
128            `template` (ReportTemplate): Report template to instantiate.
129            `debug` (bool): ?
130            `dump_values` (bool): For supported widgets, incorporate a .csv
131                file containing the underlying data used to render the widget
132
133        Returns:
134            Report top level HTML as a string.
135        """
136        self.template = template
137        self.dump_values = dump_values
138
139        # if debug is True:
140            # self.html = Debugstringio()
141
142        # else:
143            # self.html = StringIO()
144
145        self.htmls = []
146
147        # Count of the headings created so far (for heading numbering and ToC)
148        self.heading_cc = 0
149
150        # Count of subheadings withing the current heading
151        self.subheading_cc = 0
152
153        # Count of figures
154        self.figure_count = 0
155
156        # List of image filenames
157        self.images = []
158
159        self.theme = template.theme
160
161        # Copy over any static files required by the theme
162        theme_dir = Path(template.theme.__file__).parent
163        for filename in template.theme.embed:
164            # logging.debug('Embedding theme file {filename}'.format(filename=filename))
165            shutil.copy(os.path.join(str(theme_dir), str(filename)), '.')
166            self.aux_files.append(filename)
167
168        # temporary hack - sensing-start should be used, but many widgets
169        # still refer to sensing_start and this avoids breaking them
170        if 'sensing-start' in self.config:
171            self.config['sensing_start'] = self.config['sensing-start']
172
173        if 'sensing-stop' in self.config:
174            self.config['sensing_stop'] = self.config['sensing-stop']
175
176        # build the list of widgets to be rendered
177        widgets = template.widgets
178
179        # remove any that fail the <scid> condition
180        for i in range(len(widgets) - 1, -1, -1):
181            # logging.debug('Testing widget ' + str(widgets[i]))
182            if not widgets[i].cond.test(self.config):
183                logging.debug('Condition test deleting widget {w}'.format(w=widgets[i]))
184                del widgets[i]
185
186        # for w in widgets:
187            # logging.info('Preprocessing {widget}'.format(widget=w))
188            # if getattr(w, 'pre_html').__func__ != getattr(Widget, 'pre_html').__func__:
189                # logging.debug('Preprocessing ' + w.__class__.__name__)
190            # w.pre_html(self)
191
192        self.render_html(widgets)
193        self.htmls.insert(0, template.theme.preamble.format(
194            title='  <title>{title}</title>\n'.format(
195                title=self.title if self.title is not None else ''),
196            mathjax=template.theme.mathjax if template.enable_maths else '',
197            STATIC_URL=settings.STATIC_URL))
198
199        if template.theme.postscript:
200            # get the srdb version to include in the postscript 
201            self.htmls.append(template.theme.postscript.format(
202                gentime=show_time(datetime.utcnow()),
203                template=template.name,
204                srdb=get_srdb_version(sid=self.config['sid'])))
205
206
207    def write(self, handle):
208        """After the document has been rendered, write the widgets contents to `handle`."""
209        for h in self.htmls:
210            if isinstance(h, str):
211                handle.write(h)
212                # handle.write(h.encode('latin-1', 'xmlcharrefreplace'))
213
214            else:
215                handle.write(h.getvalue())
216                # handle.write(h.getvalue().encode('latin-1', 'xmlcharrefreplace'))
217
218    def append_figure(self,  # (too many args) pylint: disable=R0913
219                      filename,
220                      width=None,
221                      height=None,
222                      title=None,
223                      zoom_filename=None,
224                      zoom_width=None,
225                      zoom_height=None,
226                      live_url=None,
227                      summary_info=None,
228                      datapoint_info=None,
229                      widget=None):
230        """Add a figure to a report.
231        Will also use jquery fancybox to make a zooming thumbnail effect.
232        Also can create a summary info subpage.
233
234        Args:
235            `filename` (str): Filename of normal image. Optionally, also the filename of
236                thumbnail image in which case browser resizing will be used
237            `width` (int): Width of normal image
238            `height` (int): Height of normal image
239            `title` (str): Figure title. If omitted, this figure does not appear in the LoF
240            `zoom_filename` (str): Zoomed image shown after clicking or in the PDF export.
241                If omitted there is not zooming effect
242            `zoom_width` (int): Width of big image (not actually needed with current lightbox)
243            `zoom_height` (int): Height of big image
244            `live_url` (str): Link to live view of this figure (appears in summary subpage)
245            `summary_info` (Table or str): Figure summary information
246            `datapoint_info` (Table or str): Statistics per datapoint
247            `widget` (Widget): Widget that created the class. We show the source XML
248                and an interpreted table of configs.
249
250        Returns:
251            Nothing. Output is appended to self.html and written to subpages.
252        """
253        # moved down here because of yet another insane AIX bug.
254        # on tcprimus PIL won't import but we are only doing ingestion/stats on tcprimus
255        # so we hide this import away
256        from PIL import Image
257
258        logging.info('Inserting {filename} zoom {zoom_filename}'.format(
259            filename=filename, zoom_filename=zoom_filename))
260
261        # if filename is None:
262            # raise ConfigError('No filename specified')
263
264        # We only place ourselves into the LoF if we have a title set
265        if title is not None:
266            self.figure_count += 1
267
268        # check that full size filename is unique
269        if filename in self.aux_files:
270            raise ConfigError('Duplicate filename in report: {f}'.format(f=filename))
271
272        # check thumbnail filename is unique
273        if zoom_filename in self.aux_files:
274            raise ConfigError('Duplicate thumbnail filename in report: {f}'.format(f=filename))
275
276        if width is None or height is None:
277            width, height = Image.open(str(filename)).size
278
279        # compute thumbnail dims if none supplied
280        # this is safer (if less efficient) since our plot code may not render
281        # the precise requested size
282        if zoom_filename is not None and (zoom_width is None or zoom_height is None):
283            zoom_width, zoom_height = Image.open(str(zoom_filename)).size
284
285        self.aux_files.append(filename)
286        if zoom_filename is not None and zoom_filename != filename:
287            self.aux_files.append(zoom_filename)
288
289        if summary_info is not None and isinstance(summary_info, Table):
290            summary_info = summary_info.to_html_str()
291
292        if datapoint_info is not None and isinstance(datapoint_info, Table):
293            datapoint_info = datapoint_info.to_html_str()
294
295        summary_filename = filename.with_suffix('.html')
296        with summary_filename.open('w') as handle:
297            handle.write(
298                render_to_string(
299                    'reports/summary.html',
300                    {'preamble': self.theme.preamble.format(title='<title>Summary info</title>',
301                                                            mathjax='',
302                                                            STATIC_URL=settings.STATIC_URL),
303                     'title': title,
304                     'filename': zoom_filename or filename,
305                     'width': (zoom_width or width) // 3,  # turn the normal image into a tiny thumbnail
306                     'height': (zoom_height or height) // 3,
307                     'live_url': live_url,
308                     'summary_info': summary_info,
309                     'datapoint_info': datapoint_info,
310                     'config': widget.make_html_config() if widget is not None else None,
311                     # 'csv': None,
312                     'elem': to_html(widget.elem) if widget is not None else None,
313                     }))
314
315            self.aux_files.append(summary_filename)
316
317        self.html.write(render_to_string(
318                'reports/figure.html',
319                {'title': title,
320                 'figure_count': self.figure_count,
321                 'filename': filename,
322                 'width': width,
323                 'height': height,
324                 'zoom_filename': zoom_filename,
325                 'livelink': live_url,
326                 'summarylink': summary_filename}))