add directive for generating ICS files

Change-Id: Ia2a0f25cfa55873737ce26d1317ed8ac08bf5ddb
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2017-01-04 10:40:09 -05:00
parent e06d8ad2f4
commit 82a98d7f51
4 changed files with 158 additions and 0 deletions

145
doc/source/_exts/ics.py Normal file
View File

@ -0,0 +1,145 @@
import datetime
import json
import os
import os.path
from docutils.io import FileOutput
from docutils import nodes
from docutils.parsers import rst
from docutils.statemachine import ViewList
import icalendar
from sphinx.util.nodes import nested_parse_with_titles
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]
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()
event.add('summary', week['name'])
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()))
description = []
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
description.append(title)
if description:
event.add('description', ', '.join(description))
cal.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 setup(app):
app.info('initializing ICS extension')
app.add_directive('ics', ICS)
app.connect('doctree-resolved', doctree_resolved)

View File

@ -4,6 +4,11 @@ import datetime
import os
import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.join(os.path.abspath('.'), '_exts'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
@ -11,6 +16,7 @@ import sys
extensions = [
'openstack_releases.sphinxext',
'sphinxcontrib.datatemplates',
'ics',
]
config_generator_config_file = 'config-generator.conf'

View File

@ -8,6 +8,12 @@
:source: schedule.yaml
:template: schedule_table.tmpl
.. ics::
:source: schedule.yaml
:name: Ocata
`Subscribe to iCalendar file <schedule.ics>`__
.. note::
With the exception of the final release date and cycle-trailing

View File

@ -38,6 +38,7 @@ sphinxext =
sphinx<1.4
oslosphinx
sphinxcontrib.datatemplates
icalendar
[build_sphinx]
source-dir = doc/source