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))