1#!/usr/bin/env python3
  2
  3"""Define basic widgets for creating the structure of a report:
  4
  5- Title
  6- Heading
  7- SubHeading
  8- Paragraph.
  9"""
 10
 11import logging
 12from collections import OrderedDict
 13from datetime import datetime
 14
 15from django.template import Template
 16from django.template import Context
 17from docutils.core import publish_parts
 18
 19import chart
 20from chart.common.path import Path
 21from chart.project import settings
 22from chart.reports.widget import Widget
 23from chart.common.exceptions import ConfigError
 24from chart.reports.widget import WidgetOptionChoice
 25from chart.reports.widget import WidgetOption
 26
 27# we must give every <a> element a name attribute otherwise ckeditor
 28# helpfully strips them out
 29name_counter = 1
 30
 31
 32def expansions(document):
 33    """Prepare a standard list of {{expansions}} for all basic text widgets."""
 34    config = document.config
 35    sid = config.get('sid')
 36    if sid is not None and sid.satellite is not None:
 37        sat_name = sid.satellite.name
 38        sat_acronym = getattr(sid.satellite, 'acronym', '')
 39
 40    else:
 41        sat_name = None
 42        sat_acronym = None
 43
 44    return {'start': config.get('sensing_start'),
 45            'stop': config.get('sensing_stop'),
 46            'sid': config.get('sid'),
 47            'scid': config.get('scid'),
 48            'gs': config.get('gs'),
 49            'sat_name': sat_name,
 50            'sat_acronym': sat_acronym,
 51            'gentime': datetime.utcnow(),
 52            'settings': settings,
 53            'template': document.template,
 54            'chart': chart,
 55            }
 56
 57class TitleWidget(Widget):
 58    """Set the report title and a top level heading rendered as <h1>.
 59
 60    See <paragraph> widget for description of text expansions available."""
 61    name = 'title'
 62
 63    thumbnail = 'widgets/title.png'
 64
 65    url = 'http://tctrac/projects/chart/wiki/TitleWidget'
 66
 67    document_options = OrderedDict([
 68        ('sid', {'type': 'sid'}),
 69        ])
 70
 71    options = OrderedDict([
 72            ('text', {'type': str,
 73                      'description': 'Text to use for the report title'}),
 74            ('logo', {'type': str,
 75                      'optional': True,
 76                      'description': 'Logo filename within the theme directory'}),
 77            ('logo-position', {'type': str,
 78                          'default': 'left',
 79                               'choices': [WidgetOptionChoice(name='left'),
 80                                           WidgetOptionChoice(name='right')]}),
 81            ('logo-width', {'type': int,
 82                       'optional': True,
 83                       'unit': 'px',
 84                       'description': 'Force image width. If omitted native size is used'}),
 85            ('logo-height', {'type': int,
 86                        'optional': True,
 87                        'unit': 'px',
 88                        'description': 'Force image height. If omitted aspect ratio is preserved'}),
 89            ('date', {'type': 'string',
 90                        'optional': True,
 91                        'multiple': True,
 92                        'description': 'Set "start" to add start date and "stop" to add stop date to Title'}),
 93            ])
 94
 95    def __init__(self):
 96        super(TitleWidget, self).__init__()
 97        self.config = {}
 98
 99    def __str__(self):
100        return 'TitleWidget({text})'.format(text=self.config.get('text', ''))
101
102    def html(self, document):
103        """Final rendering pass."""
104        c = self.config
105        preloads = ('{%load link%}',)
106        document.title = Template(''.join(preloads) + self.config['text']).render(
107            Context(expansions(document)))
108
109        # ! Find where, if ever, this is used and replace the HMTL widget with
110        # a title widget
111        if 'date' in c:
112            for dates in c['date']:
113                if dates == 'start':
114                    mydate = document.config.get('sensing_start').date()
115                    document.title += ' for ' + str(mydate)
116
117                if dates == 'stop':
118                    mydate = document.config.get('sensing_stop').date()
119                    document.title += ' to ' + str(mydate)
120
121        if 'logo' in c:
122            theme_dir = Path(document.template.theme.__file__).parent
123            filename = theme_dir.child(c['logo'])
124            width = c.get('logo-width')
125            height = c.get('logo-height')
126
127            if width is None or height is None:
128                from PIL import Image
129                # Switch off spurious log messages
130                png_logger = logging.getLogger('PIL')
131                png_logger.setLevel(logging.ERROR)
132                image = Image.open(str(filename))
133                orig_width = image.size[0]
134                orig_height = image.size[1]
135
136                if width is None:
137                    width = orig_width
138
139                if height is None:
140                    height = int(orig_height * (width / orig_width) + 0.5)
141
142            if c['logo-position'] == 'left':
143                document.html.write(
144                    '<h1><img class="logo_left" src="{filename}" style="width:{width}px;'
145                    'height:{height}px;" alt="EUM logo">{title}</h1>\n'.format(
146                        filename=c['logo'],
147                        width=width,
148                        height=height,
149                        title=document.title))
150
151            elif c['logo-position'] == 'right':
152                document.html.write(
153                    '<img class="logo_right" src="{filename}" style="width:{width}px;'
154                    'height:{height}px" alt="EUM logo"><h1>{title}</h1>\n'.format(
155                        filename=c['logo'],
156                        width=width,
157                        height=height,
158                        title=document.title))
159
160            else:
161                raise ConfigError('Unknown logo-position: {p}'.format(p=c['logo-position']))
162
163            theme_dir = Path(document.template.theme.__file__).parent
164            theme_dir.joinpath(c['logo']).copy('.')
165            document.aux_files.append(c['logo'])
166
167        else:
168            document.html.write('<h1>{title}</h1>\n'.format(title=document.title))
169
170
171class HeadingWidget(Widget):
172    """A section heading, rendered as <h2>.
173
174    See <paragraph> widget for description of text expansions available."""
175    name = 'heading'
176
177    thumbnail = 'widgets/heading.png'
178
179    options = OrderedDict([
180            ('text', {'type': 'string',
181                      'description': 'Heading text'}),
182            ('page-break', {'type': bool,
183                            'optional': True,
184                            'description': 'Include a page break before this heading'}),
185            ])
186
187    # options = OrderedDict((('text', {'type': 'string',
188                                     # 'description': 'Heading text'}),))
189
190    def __init__(self):
191        super(HeadingWidget, self).__init__()
192        # our heading number within the document
193        self.heading = None
194        self.config = {}
195
196    def __str__(self):
197        return 'HeadingWidget({text})'.format(text=self.config.get('text', ''))
198
199    def pre_html(self, document):
200        """Set up TOC entry."""
201        c = self.config
202
203        preloads = ('{%load link%}',)
204        c['text'] = Template(''.join(preloads) + self.config['text']).render(
205            Context(expansions(document)))
206
207        document.headings.append(c['text'])
208        document.subheadings.append([])
209        self.heading = len(document.headings)
210
211    def html(self, document):
212        """Render ourselves."""
213        # global name_counter
214        c = self.config
215        if document.heading_cc is None:
216            document.html.write("\n<h2>{text}</h2>\n".format(
217                    text=c['text']))
218
219        else:
220            document.heading_cc += 1
221            # name attribute is required by ckeditor
222            document.html.write(
223                '\n<h2 {brk}><a name="heading-{heading}" id="heading-{heading}" '
224                'class="anchor"></a>{heading}. {text}</h2>\n'.format(
225                    # name='dummy' + str(name_counter),
226                    heading=self.heading,
227                    brk=' style="page-break-before: always"' if c.get('page-break') else '',
228                    text=c['text']))
229
230            # name_counter += 1
231
232
233class SubHeadingWidget(Widget):
234    """A subheading widget, rendered as <h3>.
235
236    See <paragraph> widget for description of text expansions available."""
237    name = 'subheading'
238
239    thumbnail = 'widgets/subheading.png'
240
241    options = OrderedDict([
242            ('text', {'type': 'string'}),
243            ('page-break', {'type': bool,
244                            'optional': True,
245                            'description': 'Include a page break before this heading'}),
246            ])
247
248    def __init__(self):
249        super(SubHeadingWidget, self).__init__()
250        self.heading = None
251        self.subheading = None
252        self.config = {}
253
254    def __str__(self):
255        return 'SubHeadingWidget({text})'.format(text=self.config.get('text', ''))
256
257    def pre_html(self, document):
258        """Initial pass. Write information about ourselves into the document structure
259        so a TableOfContents can retrieve it.
260        """
261
262        c = self.config
263
264        preloads = ('{%load link%}',)
265        c['text'] = Template(''.join(preloads) + self.config['text']).render(
266            Context(expansions(document)))
267
268        self.heading = len(document.headings)
269        if self.heading > 0:
270            document.subheadings[self.heading - 1].append(c['text'])
271            self.subheading = len(document.subheadings[self.heading - 1])
272
273    def html(self, document):
274        """Render ourselves."""
275        # global name_counter
276        c = self.config
277        # text = c['text'].format(document=document.config)
278        if self.heading != 0:
279            # the report has already written a Heading we use proper numbering ...
280            document.subheading_cc += 1
281            document.html.write(
282                '\n<h3{brk}><a name="subheading-{heading}-{subheading}" '
283                'id="subheading-{heading}-{subheading}" class="anchor"></a>'
284                '{heading}.{subheading}. {text}</h3>\n'.format(
285                    # name='dummy' + str(name_counter),
286                    heading=self.heading,
287                    brk=' style="page-break-before: always"' if c.get('page-break') else '',
288                    subheading=self.subheading,
289                    text=c['text']))
290            # name_counter += 1
291
292        else:
293            # ... otherwise we write a plain heading without numbering
294            logging.warning('Found a SubHeading without a previous Heading')
295            document.html.write('\n<h3{brk}>{text}</h3>\n'.format(
296                    brk=' style="page-break-before: always"' if c.get('page-break') else '',
297                    text=c['text']))
298
299
300class SubSubHeadingWidget(Widget):
301    """A subheading widget, rendered as <h4>.
302    Does not appear in table of contents and subsubheadings are not numbered.
303
304    See <paragraph> widget for description of text expansions available."""
305    name = 'subsubheading'
306
307    thumbnail = 'widgets/subheading.png'
308
309    options = OrderedDict([
310            ('text', {'type': 'string'})])
311
312    def __str__(self):
313        return 'SubSubHeadingWidget({text})'.format(text=self.config.get('text', ''))
314
315    def html(self, document):
316        """Render ourselves."""
317        c = self.config
318        # allow substitutions such as {scid}
319        # text = c['text'].format(document=document.config)
320        preloads = ('{%load link%}',)
321        c['text'] = Template(''.join(preloads) + self.config['text']).render(
322            Context(expansions(document)))
323
324        document.html.write('\n<h4>{text}</h4>\n'.format(
325                text=c['text']))
326
327
328class ParagraphWidget(Widget):
329    """Insert one or more paragraphs of text into a report.
330
331    Strings are expanded using the Django template engine. The following variables are available:
332
333     * {{scid}}: Retrieve Spacecraft-ID for projects that use them
334     * {{start}}: Start time of the report period
335     * {{stop}}: Stop time of the report period
336     * {{sid}}: Name of the data source (spacecraft ID, ground station ID, or other)
337     * {{sat_name}}: Name of the underlying spacecraft, if available (usually same as SID
338       but could differ if for example, SID was a validation source then sid might be "M02 (VAL)"
339       and sat_name might be "M02"
340     * {{sat_acryonm}}: For CHART-EPS only gives an abbreviated spacecraft name. In practice
341       same as sid
342     * {{gentime}}: Timestamp when the report is being created
343     * {{settings}}: Data structure holding the project settings i.e. settings.port to give
344       web server IP port
345     * {{template}}: Name of the report XML file being generated
346     * {{chart}}: Access to the chart module, used to give versions i.e. chart.version
347
348    Also available is the {%link%} command for intra-report links - see wiki page for more details.
349    """
350    name = 'paragraph'
351
352    url = 'http://tctrac/projects/chart/wiki/ParagraphWidget'
353
354    image = 'widgets/paragraph.png'
355    thumbnail = 'widgets/paragraph_sm.png'
356
357    options = [WidgetOption(name='text',
358                            datatype=str,
359                            description='Plain text paragraph',
360                            multiple=True),
361               WidgetOption(name='rst',
362                            datatype=str,
363                            description='RestructuredText paragraph',
364                            multiple=True),]
365
366    document_options = OrderedDict([
367            ('sensing_start', {'type': 'datetime'}),
368            ('sensing_stop', {'type': 'datetime'})])
369
370    def __init__(self):
371        super(ParagraphWidget, self).__init__()
372
373    def __str__(self):
374        return 'ParagraphWidget'
375
376    def html(self, document):
377        """Render ourselves."""
378        c = self.config
379        exp = expansions(document)
380        for p in c['text']:
381            preloads = ('{%load link%}',)
382            # document.html.write('<p>' + p.format(document=document.config) + '</p>\n')
383            document.html.write('<p>' +
384                                Template(''.join(preloads) + p).render(Context(exp)) +
385                                '</p>\n')
386
387        if 'rst' in c:
388            for r in c['rst']:
389            # Remove the initial tab from each docstring line
390                shrunken = []
391                for line in r.split('\n'):
392                    while line.startswith('\t'):
393                        line = line[1:]
394
395                    line = Template(line).render(Context(exp))
396
397                    shrunken.append(line)
398
399                # Convert the raw docstring text into HTML via a restructured-text processor
400                rst = publish_parts(source='\n'.join(shrunken),
401                                    writer_name='html4css1')['fragment']
402                document.html.write(rst)
403
404
405class LayoutWidget(Widget):
406    """    """
407    name = 'layout'
408
409    # url = settings.CORE_WIKI_URL + '/ParagraphWidget'
410    # image = 'widgets/paragraph.png'
411    # thumbnail = 'widgets/paragraph_sm.png'
412
413    options = [WidgetOption(
414        name='type',
415        datatype=str,
416        description='Layout action',
417        choices=[
418            WidgetOptionChoice(name='horizontal-begin',
419                               description='Start a row of horizontal widgets'),
420            WidgetOptionChoice(name='horizontal-continue',
421                               description='Continue a row of horizontal widgets'),
422            WidgetOptionChoice(name='horizontal-end',
423                               description='Read from TMREP database'),
424        ])]
425
426    def __str__(self):
427        return 'layout'
428
429    def html(self, document):
430        """Render ourselves."""
431        c = self.config
432        html = document.html
433        layout_type = c['type']
434        if layout_type == 'horizontal-begin':
435            html.write('<table style="border-collapse: collapse;border:none"><tr style="border:none"><td style="border:none">\n')
436
437        elif layout_type == 'horizontal-continue':
438            html.write('</td><td style="border:none">\n')
439
440        elif layout_type == 'horizontal-end':
441            html.write('</td></tr></table>\n')
442
443        else:
444            raise ValueError('Unrecognised layout type {t}'.format(t=layout_type))