releases/doc/source/_exts/ics.py
Doug Hellmann bcd46791a6 fix line breaks in calendar event descriptions
Remove line breaks within a paragraph to allow calendar software to
handle the text formatting.

Change-Id: Ic4c335b9bff7f0213f22c821059d9e0a842b859b
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2017-01-11 13:54:38 -05:00

193 lines
6.2 KiB
Python

import datetime
import json
import os
import os.path
from docutils.io import FileOutput
from docutils import nodes
from docutils.parsers import rst
import icalendar
import yaml
class PendingICS(nodes.Element):
def __init__(self, data_source, series_name, data):
super(PendingICS, self).__init__()
self._data_source = data_source
self._series_name = series_name
self._data = data
class ICS(rst.Directive):
option_spec = {
'source': rst.directives.unchanged,
'name': rst.directives.unchanged,
}
has_content = False
def _load_data(self, env, data_source):
rel_filename, filename = env.relfn2path(data_source)
if data_source.endswith('.yaml'):
with open(filename, 'r') as f:
return yaml.load(f)
elif data_source.endswith('.json'):
with open(filename, 'r') as f:
return json.load(f)
else:
raise NotImplementedError('cannot load file type of %s' %
data_source)
def run(self):
env = self.state.document.settings.env
try:
data_source = self.options['source']
except KeyError:
error = self.state_machine.reporter.error(
'No source set for ics directive',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
try:
series_name = self.options['name']
except KeyError:
error = self.state_machine.reporter.error(
'No name set for ics directive',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
data = self._load_data(env, data_source)
node = PendingICS(data_source, series_name, data)
node.document = self.state.document
return [node]
_global_calendar = icalendar.Calendar()
_global_calendar.add('prodid', '-//releases.openstack.org//EN')
_global_calendar.add('X-WR-CALNAME', 'OpenStack Release Schedule')
def _format_description(node):
"Given a node, get its text and remove line breaks in paragraphs."
text = node.astext()
parts = text.split('\n\n')
return '\n\n'.join(p.replace('\n', ' ') for p in parts)
def doctree_resolved(app, doctree, docname):
builder = app.builder
for node in doctree.traverse(PendingICS):
series_name = node._series_name
data = node._data
app.info('building {} calendar'.format(series_name))
cal = icalendar.Calendar()
cal.add('prodid', '-//releases.openstack.org//EN')
cal.add('X-WR-CALNAME', '{} schedule'.format(series_name))
for week in data['cycle']:
if not week.get('name'):
continue
event = icalendar.Event()
summary = []
for item in week.get('x-project', []):
try:
# Look up the cross-reference name to get the
# section, then get the title from the first child
# node.
title = doctree.ids[item].children[0].astext()
except Exception as e:
# NOTE(dhellmann): Catching "Exception" is a bit
# ugly, but given the complexity of the expression
# above there are a bunch of ways things might
# fail.
app.info('could not get title for {}: {}'.format(item, e))
title = item
summary.append(title)
if summary:
summary_text = ' (' + '; '.join(summary) + ')'
else:
summary_text = ''
event.add(
'summary',
'{} {}{}'.format(series_name.title(),
week['name'],
summary_text),
)
start = datetime.datetime.strptime(week['start'], '%Y-%m-%d')
event.add('dtstart', icalendar.vDate(start.date()))
# NOTE(dhellmann): ical assumes a time of midnight, so in
# order to have the event span the final day of the week
# we have to add an extra day.
raw_end = datetime.datetime.strptime(week['end'], '%Y-%m-%d')
end = raw_end + datetime.timedelta(days=1)
event.add('dtend', icalendar.vDate(end.date()))
# Look up the cross-reference name to get the
# section, then add the full description to the
# text.
description = [
_format_description(doctree.ids[item])
for item in week.get('x-project', [])
]
if description:
event.add('description', '\n\n'.join(description))
cal.add_component(event)
_global_calendar.add_component(event)
output_full_name = os.path.join(
builder.outdir,
docname + '.ics',
)
output_dir_name = os.path.dirname(output_full_name)
if not os.path.exists(output_dir_name):
os.makedirs(output_dir_name)
destination = FileOutput(
destination_path=output_full_name,
encoding='utf-8',
)
app.info('generating {}'.format(output_full_name))
destination.write(cal.to_ical())
# Remove the node that the writer won't understand.
node.parent.replace(node, [])
def build_finished(app, exception):
if exception is not None:
return
builder = app.builder
output_full_name = os.path.join(
builder.outdir,
'schedule.ics',
)
output_dir_name = os.path.dirname(output_full_name)
if not os.path.exists(output_dir_name):
os.makedirs(output_dir_name)
destination = FileOutput(
destination_path=output_full_name,
encoding='utf-8',
)
app.info('generating {}'.format(output_full_name))
destination.write(_global_calendar.to_ical())
def setup(app):
app.info('initializing ICS extension')
app.add_directive('ics', ICS)
app.connect('doctree-resolved', doctree_resolved)
app.connect('build-finished', build_finished)