events.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. # -*- coding: utf-8 -*-
  2. """
  3. events plugin for Pelican
  4. =========================
  5. This plugin looks for and parses an "events" directory and generates
  6. blog posts with a user-defined event date. (typically in the future)
  7. It also generates an ICalendar v2.0 calendar file.
  8. https://en.wikipedia.org/wiki/ICalendar
  9. Author: Federico Ceratto <federico.ceratto@gmail.com>
  10. Released under AGPLv3+ license, see LICENSE
  11. """
  12. from datetime import datetime, timedelta
  13. from pelican import signals, utils
  14. import icalendar
  15. import logging
  16. import os.path
  17. import pytz
  18. log = logging.getLogger(__name__)
  19. TIME_MULTIPLIERS = {
  20. 'w': 'weeks',
  21. 'd': 'days',
  22. 'h': 'hours',
  23. 'm': 'minutes',
  24. 's': 'seconds'
  25. }
  26. events = []
  27. localized_events = {}
  28. def parse_tstamp(ev, field_name):
  29. """Parse a timestamp string in format "YYYY-MM-DD HH:MM"
  30. :returns: datetime
  31. """
  32. try:
  33. return datetime.strptime(ev[field_name], '%Y-%m-%d %H:%M')
  34. except Exception, e:
  35. log.error("Unable to parse the '%s' field in the event named '%s': %s" \
  36. % (field_name, ev['title'], e))
  37. raise
  38. def parse_timedelta(ev):
  39. """Parse a timedelta string in format [<num><multiplier> ]*
  40. e.g. 2h 30m
  41. :returns: timedelta
  42. """
  43. chunks = ev['event-duration'].split()
  44. tdargs = {}
  45. for c in chunks:
  46. try:
  47. m = TIME_MULTIPLIERS[c[-1]]
  48. val = int(c[:-1])
  49. tdargs[m] = val
  50. except KeyError:
  51. log.error("""Unknown time multiplier '%s' value in the \
  52. 'event-duration' field in the '%s' event. Supported multipliers \
  53. are: '%s'.""" % (c, ev['title'], ' '.join(TIME_MULTIPLIERS)))
  54. raise RuntimeError("Unknown time multiplier '%s'" % c)
  55. except ValueError:
  56. log.error("""Unable to parse '%s' value in the 'event-duration' \
  57. field in the '%s' event.""" % (c, ev['title']))
  58. raise ValueError("Unable to parse '%s'" % c)
  59. return timedelta(**tdargs)
  60. def parse_article(generator, metadata):
  61. """Collect articles metadata to be used for building the event calendar
  62. :returns: None
  63. """
  64. if 'event-start' not in metadata:
  65. return
  66. dtstart = parse_tstamp(metadata, 'event-start')
  67. if 'event-end' in metadata:
  68. dtend = parse_tstamp(metadata, 'event-end')
  69. elif 'event-duration' in metadata:
  70. dtdelta = parse_timedelta(metadata)
  71. dtend = dtstart + dtdelta
  72. else:
  73. msg = "Either 'event-end' or 'event-duration' must be" + \
  74. " speciefied in the event named '%s'" % metadata['title']
  75. log.error(msg)
  76. raise ValueError(msg)
  77. events.append((dtstart, dtend, metadata))
  78. def generate_ical_file(generator):
  79. """Generate an iCalendar file
  80. """
  81. global events
  82. ics_fname = generator.settings['PLUGIN_EVENTS']['ics_fname']
  83. if not ics_fname:
  84. return
  85. ics_fname = os.path.join(generator.settings['OUTPUT_PATH'], ics_fname)
  86. log.debug("Generating calendar at %s with %d events" % (ics_fname, len(events)))
  87. tz = generator.settings.get('TIMEZONE', 'UTC')
  88. tz = pytz.timezone(tz)
  89. ical = icalendar.Calendar()
  90. ical.add('prodid', '-//My calendar product//mxm.dk//')
  91. ical.add('version', '2.0')
  92. multiLanguageSupportNecessary = "i18n_subsites" in generator.settings["PLUGINS"]
  93. if multiLanguageSupportNecessary:
  94. currentLang = os.path.basename(os.path.normpath(generator.settings['OUTPUT_PATH']))
  95. sortedUniqueEvents = sorted(events)
  96. last = sortedUniqueEvents[-1]
  97. for i in range(len(sortedUniqueEvents)-2, -1,-1):
  98. if last == sortedUniqueEvents[i]:
  99. del sortedUniqueEvents[i]
  100. else:
  101. last = sortedUniqueEvents[i]
  102. localized_events[currentLang] = []
  103. else:
  104. sortedUniqueEvents = events
  105. for e in sortedUniqueEvents:
  106. dtstart, dtend, metadata = e
  107. if multiLanguageSupportNecessary:
  108. if currentLang != metadata['lang']:
  109. log.debug("%s is not equal to %s" %(currentLang, metadata['lang']))
  110. continue
  111. elif currentLang == generator.settings['DEFAULT_LANG']:
  112. localized_events[currentLang].append(e)
  113. ie = icalendar.Event(
  114. summary=metadata['summary'],
  115. dtstart=dtstart,
  116. dtend=dtend,
  117. dtstamp=metadata['date'],
  118. priority=5,
  119. uid=metadata['title'] + metadata['summary'],
  120. )
  121. if 'event-location' in metadata:
  122. ie.add('location', metadata['event-location'])
  123. ical.add_component(ie)
  124. if localized_events:
  125. localized_events[currentLang] = sorted(localized_events[currentLang], reverse=True9)
  126. if not os.path.exists(generator.settings['OUTPUT_PATH']):
  127. os.makedirs(generator.settings['OUTPUT_PATH'])
  128. with open(ics_fname, 'wb') as f:
  129. f.write(ical.to_ical())
  130. def generate_events_list(generator):
  131. """Populate the event_list variable to be used in jinja templates"""
  132. if not localized_events:
  133. generator.context['events_list'] = sorted(events, reverse = True)
  134. else:
  135. generator.context['events_list'] = localized_events
  136. def register():
  137. signals.article_generator_context.connect(parse_article)
  138. signals.article_generator_finalized.connect(generate_ical_file)
  139. signals.article_generator_finalized.connect(generate_events_list)