add directive for generating ICS files
Change-Id: Ia2a0f25cfa55873737ce26d1317ed8ac08bf5ddb Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
parent
e06d8ad2f4
commit
82a98d7f51
|
@ -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)
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue