Browse Source
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 icalmaster
rra
7 years ago
4 changed files with 327 additions and 0 deletions
@ -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 |
@ -0,0 +1 @@ |
|||
from .events import * |
@ -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) |
|||
|
|||
|
@ -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