roels' flavour
added switch for multiple calendars based on categories added support for multiple calendars based on categories, added support for setting a default location added support for date ranges and fixed incorrect conversions to ical
This commit is contained in:
parent
90ac86c714
commit
6c0f5a0c1b
62
Readme.rst
Normal file
62
Readme.rst
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
events
|
||||||
|
----
|
||||||
|
|
||||||
|
This plugin scans blog posts for an events.
|
||||||
|
It also generates an ``.ical`` calendar file.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
This plugin depends on the ``icalendar`` package, which can be installed
|
||||||
|
using APT or RPM or, if you are unlucky, via pip::
|
||||||
|
|
||||||
|
pip install icalendar
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Add the following to pelicanconf.py::
|
||||||
|
PLUGIN_EVENTS = {
|
||||||
|
'ics_calendars': ['events.ics', 'meetings.ics'],
|
||||||
|
'default_location' : '',
|
||||||
|
'calendar_per_category': False
|
||||||
|
}
|
||||||
|
|
||||||
|
Create articles and usual and add the "event-start" metadata to turn them into
|
||||||
|
events. The event start is independent of the article "date".
|
||||||
|
"event-start" is in "YYYY-MM-DD hh:mm" format.
|
||||||
|
Also add "event-end", in the same format, or "event-duration" as a number
|
||||||
|
followed by a dimension:
|
||||||
|
|
||||||
|
w: weeks
|
||||||
|
d: days
|
||||||
|
h: hours
|
||||||
|
m: minutes
|
||||||
|
s: seconds
|
||||||
|
|
||||||
|
You can also add recurring events by setting the 'event-recurring' setting. Use 'daily', 'weekly', 'monthly' or 'yearly'. These will continue untill a set end date in "YYYY-MM-DD hh:mm" format.
|
||||||
|
|
||||||
|
Specify an optional "location" or set a default location in pelicanconf.py
|
||||||
|
|
||||||
|
Example in ReST format::
|
||||||
|
|
||||||
|
:event-start: 2015-01-21 10:30
|
||||||
|
:event-duration: 2h
|
||||||
|
:event-recurring: weekly untill 2015-12-21
|
||||||
|
:location: somewhere
|
||||||
|
|
||||||
|
Calendar per Category:
|
||||||
|
If you set the calendar_per_category setting to True, the plugin will create a calendar for every category you list both in ics_calendar and the article metadata.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
To generate an sorted event list in a dedicated page copy the events_list.html
|
||||||
|
template under the templates directory in your theme, then create a page:
|
||||||
|
|
||||||
|
content/pages/events_list.rst::
|
||||||
|
|
||||||
|
Events list
|
||||||
|
###########
|
||||||
|
:slug: events-list
|
||||||
|
:summary:
|
||||||
|
:template: events_list
|
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .events import *
|
228
events.py
Normal file
228
events.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
events plugin for Pelican
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This plugin looks for and parses an "events" directory and generates
|
||||||
|
blog posts with a user-defined event date. (typically in the future)
|
||||||
|
It also generates an ICalendar v2.0 calendar file.
|
||||||
|
https://en.wikipedia.org/wiki/ICalendar
|
||||||
|
|
||||||
|
|
||||||
|
Author: Federico Ceratto <federico.ceratto@gmail.com>
|
||||||
|
Released under AGPLv3+ license, see LICENSE
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pelican import signals, utils
|
||||||
|
from collections import namedtuple, defaultdict
|
||||||
|
import icalendar
|
||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TIME_MULTIPLIERS = {
|
||||||
|
'w': 'weeks',
|
||||||
|
'd': 'days',
|
||||||
|
'h': 'hours',
|
||||||
|
'm': 'minutes',
|
||||||
|
's': 'seconds'
|
||||||
|
}
|
||||||
|
|
||||||
|
events = []
|
||||||
|
localized_events = defaultdict(list)
|
||||||
|
Event = namedtuple("Event", "dtstart dtend metadata rrule")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tstamp(ev, field_name):
|
||||||
|
"""Parse a timestamp string in format "YYYY-MM-DD HH:MM"
|
||||||
|
|
||||||
|
:returns: datetime
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return datetime.strptime(ev[field_name], '%Y-%m-%d %H:%M')
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Unable to parse the '%s' field in the event named '%s': %s" \
|
||||||
|
% (field_name, ev['title'], e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def parse_timedelta(ev):
|
||||||
|
"""Parse a timedelta string in format [<num><multiplier> ]*
|
||||||
|
e.g. 2h 30m
|
||||||
|
|
||||||
|
:returns: timedelta
|
||||||
|
"""
|
||||||
|
|
||||||
|
chunks = ev['event-duration'].split()
|
||||||
|
tdargs = {}
|
||||||
|
for c in chunks:
|
||||||
|
try:
|
||||||
|
m = TIME_MULTIPLIERS[c[-1]]
|
||||||
|
val = int(c[:-1])
|
||||||
|
tdargs[m] = val
|
||||||
|
except KeyError:
|
||||||
|
log.error("""Unknown time multiplier '%s' value in the \
|
||||||
|
'event-duration' field in the '%s' event. Supported multipliers \
|
||||||
|
are: '%s'.""" % (c, ev['title'], ' '.join(TIME_MULTIPLIERS)))
|
||||||
|
raise RuntimeError("Unknown time multiplier '%s'" % c)
|
||||||
|
except ValueError:
|
||||||
|
log.error("""Unable to parse '%s' value in the 'event-duration' \
|
||||||
|
field in the '%s' event.""" % (c, ev['title']))
|
||||||
|
raise ValueError("Unable to parse '%s'" % c)
|
||||||
|
|
||||||
|
|
||||||
|
return timedelta(**tdargs)
|
||||||
|
|
||||||
|
def parse_recursion(ev, field_name):
|
||||||
|
"""
|
||||||
|
Parse information about recurring events and return frequency of event and untill time.
|
||||||
|
"""
|
||||||
|
chunks = ev[field_name].split()
|
||||||
|
freq = chunks[0].upper()
|
||||||
|
if 'until' in chunks:
|
||||||
|
until = datetime.strptime(chunks[-1], '%Y-%m-%d')
|
||||||
|
rrule = [freq, until]
|
||||||
|
else:
|
||||||
|
rrule = [freq]
|
||||||
|
return rrule
|
||||||
|
|
||||||
|
def parse_article(generator, metadata):
|
||||||
|
"""Collect articles metadata to be used for building the event calendar
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
if 'event-start' not in metadata:
|
||||||
|
return
|
||||||
|
|
||||||
|
dtstart = parse_tstamp(metadata, 'event-start')
|
||||||
|
rrule=[]
|
||||||
|
if 'event-end' in metadata:
|
||||||
|
dtend = parse_tstamp(metadata, 'event-end')
|
||||||
|
|
||||||
|
elif 'event-duration' in metadata:
|
||||||
|
dtdelta = parse_timedelta(metadata)
|
||||||
|
dtend = dtstart + dtdelta
|
||||||
|
|
||||||
|
if 'event-recurring' in metadata:
|
||||||
|
rrule = parse_recursion(metadata, 'event-recurring')
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = "Either 'event-end' or 'event-duration' must be" + \
|
||||||
|
" speciefied in the event named '%s'" % metadata['title']
|
||||||
|
log.error(msg)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
events.append(Event(dtstart, dtend, metadata, rrule))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ical_file(generator):
|
||||||
|
"""Generate an iCalendar file
|
||||||
|
"""
|
||||||
|
from icalendar import vDatetime as vd
|
||||||
|
|
||||||
|
global events
|
||||||
|
|
||||||
|
ics_calendars = generator.settings['PLUGIN_EVENTS']['ics_calendars']
|
||||||
|
default_loc = generator.settings['PLUGIN_EVENTS']['default_location']
|
||||||
|
category_calendar = generator.settings['PLUGIN_EVENTS']['calendar_per_category']
|
||||||
|
|
||||||
|
if not ics_calendars:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not category_calendar:
|
||||||
|
ics_calendars = ics_calendars[:1]
|
||||||
|
|
||||||
|
for calendar in ics_calendars:
|
||||||
|
|
||||||
|
ics_fname = os.path.join(generator.settings['OUTPUT_PATH'], calendar)
|
||||||
|
log.debug("Generating calendar at %s with %d events" % (ics_fname, len(events)))
|
||||||
|
|
||||||
|
tz = generator.settings.get('TIMEZONE', 'UTC')
|
||||||
|
tz = pytz.timezone(tz)
|
||||||
|
|
||||||
|
ical = icalendar.Calendar()
|
||||||
|
ical.add('prodid', '-//My calendar product//mxm.dk//')
|
||||||
|
ical.add('version', '2.0')
|
||||||
|
|
||||||
|
DEFAULT_LANG = generator.settings['DEFAULT_LANG']
|
||||||
|
curr_events = events if not localized_events else localized_events[DEFAULT_LANG]
|
||||||
|
for e in curr_events:
|
||||||
|
|
||||||
|
if str(e.metadata['category']) in calendar or not category_calendar:
|
||||||
|
#force convert to ical format here, because otherwise it doesn't happen?
|
||||||
|
dtend = vd(e.dtend).to_ical()
|
||||||
|
dtstart = vd(e.dtstart).to_ical()
|
||||||
|
dtstamp = vd (e.metadata['date']).to_ical()
|
||||||
|
|
||||||
|
# print('*'*10)
|
||||||
|
print(e.metadata['category'])
|
||||||
|
|
||||||
|
ie = icalendar.Event(
|
||||||
|
summary=e.metadata['title'],
|
||||||
|
dtstart=dtstart,
|
||||||
|
dtend=dtend,
|
||||||
|
dtstamp= dtstamp,
|
||||||
|
priority=5,
|
||||||
|
uid=e.metadata['title'],
|
||||||
|
)
|
||||||
|
if 'event-location' in e.metadata:
|
||||||
|
ie.add('location', e.metadata['event-location'])
|
||||||
|
elif default_loc:
|
||||||
|
ie.add('location', default_loc)
|
||||||
|
|
||||||
|
|
||||||
|
if 'event-recurring' in e.metadata:
|
||||||
|
if len(e.rrule)>=2:
|
||||||
|
ie.add('rrule', {'freq':e.rrule[0],'until':e.rrule[1]})
|
||||||
|
else:
|
||||||
|
ie.add('rrule', {'freq':e.rrule[0]})
|
||||||
|
|
||||||
|
ical.add_component(ie)
|
||||||
|
with open(ics_fname, 'wb') as f:
|
||||||
|
f.write(ical.to_ical())
|
||||||
|
|
||||||
|
|
||||||
|
def generate_localized_events(generator):
|
||||||
|
""" Generates localized events dict if i18n_subsites plugin is active """
|
||||||
|
|
||||||
|
if "i18n_subsites" in generator.settings["PLUGINS"]:
|
||||||
|
if not os.path.exists(generator.settings['OUTPUT_PATH']):
|
||||||
|
os.makedirs(generator.settings['OUTPUT_PATH'])
|
||||||
|
|
||||||
|
for e in events:
|
||||||
|
if "lang" in e.metadata:
|
||||||
|
localized_events[e.metadata["lang"]].append(e)
|
||||||
|
else:
|
||||||
|
log.debug("event %s contains no lang attribute" % (e.metadata["title"],))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_events_list(generator):
|
||||||
|
"""Populate the event_list variable to be used in jinja templates"""
|
||||||
|
|
||||||
|
if not localized_events:
|
||||||
|
generator.context['events_list'] = sorted(events, reverse = True,
|
||||||
|
key=lambda ev: (ev.dtstart, ev.dtend))
|
||||||
|
else:
|
||||||
|
generator.context['events_list'] = {k: sorted(v, reverse = True,
|
||||||
|
key=lambda ev: (ev.dtstart, ev.dtend))
|
||||||
|
for k, v in localized_events.items()}
|
||||||
|
|
||||||
|
def initialize_events(article_generator):
|
||||||
|
"""
|
||||||
|
Clears the events list before generating articles to properly support plugins with
|
||||||
|
multiple generation passes like i18n_subsites
|
||||||
|
"""
|
||||||
|
|
||||||
|
del events[:]
|
||||||
|
localized_events.clear()
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.article_generator_init.connect(initialize_events)
|
||||||
|
signals.article_generator_context.connect(parse_article)
|
||||||
|
signals.article_generator_finalized.connect(generate_localized_events)
|
||||||
|
signals.article_generator_finalized.connect(generate_ical_file)
|
||||||
|
signals.article_generator_finalized.connect(generate_events_list)
|
||||||
|
|
||||||
|
|
36
events_list.html
Normal file
36
events_list.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %} Events list - {{ SITENAME }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if events_list %}
|
||||||
|
<ul class="post-list">
|
||||||
|
{% for evstart, evend, event in events_list %}
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
<a href="../{{event.slug}}.html">
|
||||||
|
<b>{{event['title']}}</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% if evstart.date() == evend.date() %}
|
||||||
|
From {{evstart}} to {{evend.time()}}
|
||||||
|
{% else %}
|
||||||
|
From {{evstart}} to {{evend}}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if event.location %}
|
||||||
|
<p>Location: {{event.location}}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>{{event['summary']}}</p>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user