summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrsiddharth <rsd@gnu.org>2016-03-18 22:34:00 -0400
committerrsiddharth <rsd@gnu.org>2016-03-18 22:34:00 -0400
commit2c2b37cfd3a018b1a6ff15b275c3768dee153d08 (patch)
treef00e162ea1651bba75ac0423f7afa34281f0bcc2
parent7f6af7acf2bdeb216ed6db847d98eeaf13326010 (diff)
Initial version of LPiCal class done.
Addresses issue #8.
-rw-r--r--lps_gen.py139
-rw-r--r--setup.py2
-rw-r--r--tests/test_lps_gen.py170
3 files changed, 294 insertions, 17 deletions
diff --git a/lps_gen.py b/lps_gen.py
index f6696b6..352d0b6 100644
--- a/lps_gen.py
+++ b/lps_gen.py
@@ -22,14 +22,19 @@ import json
import re
import sys
+import pytz
+
from argparse import ArgumentParser
from collections import OrderedDict
+from datetime import datetime
from os import path
from bs4 import BeautifulSoup
+from icalendar import Calendar, Event, vCalAddress, vText, vDatetime
from jinja2 import Environment, FileSystemLoader
from jinja2.exceptions import TemplateNotFound
from mistune import Renderer, Markdown
+from pytz import timezone
from unidecode import unidecode
from lpschedule_generator._version import __version__
@@ -108,14 +113,36 @@ class LPiCal(object):
Used for producing iCal for LP schedule.
"""
- def __init__(self, lps_dict):
+ def __init__(self, lps_dict, lp_year):
self.lps_dict = lps_dict
+ self.lp_year = lp_year
# Matches strings like '09:45 - 10:30: Lorem ipsum dolor sit.'
- self.timeslot_re = re.compile(r'(\d+:\d+).+?(\d+:\d+)')
+ self.timeslot_re = re.compile(r'(\d+:\d+).+?(\d+:\d+):\s*(.+\b)')
# Matches strings like 'Saturday, March 19'
self.month_day_re = re.compile(r'\w+,\s*([a-zA-Z]+)\s*(\d+)')
+ self.cal = Calendar()
+ self.cal.add('prodid', '-//lpschedule generator//mxm.dk//')
+ self.cal.add('version', '2.0')
+
+ # RFC 2445 requires DTSTAMP to be in UTC. DTSTAMP is used in
+ # VEVENT (Event object, see `add_event` method).
+ self.dtstamp = vDatetime(datetime.now(pytz.utc))
+
+ # used to generate uid for ical.
+ self.ucounter = 0
+
+
+ def gen_uid(self):
+ """Returns an unique id.
+
+ Used for Event object.
+ """
+ self.ucounter = self.ucounter + 1
+ return '%s@LP%s@libreplanet.org' % (str(self.ucounter),
+ self.lp_year)
+
def get_timeslot(self, s):
"""Get start and end time for a timeslot.
@@ -123,10 +150,11 @@ class LPiCal(object):
timeslot = self.timeslot_re.search(s)
- start = timeslot.group(1)
- end = timeslot.group(2)
+ t_start = timeslot.group(1)
+ t_end = timeslot.group(2)
+ name = timeslot.group(3)
- return start, end
+ return t_start, t_end, name
def get_month_day(self, s):
@@ -141,6 +169,107 @@ class LPiCal(object):
return month, day
+ def mk_datetime(self, month, day, time):
+ """Returns datetime object (EST).
+ """
+ # Day %d
+ # Month %B
+ # Year %Y
+ # Hour %H (24-hr)
+ # Minute %M (zero padded)
+ # Second %S (zero padded)
+ datetime_fmt = '%d %B %Y %H:%M:%S'
+ eastern = timezone('US/Eastern')
+
+ hour = time.split(':')[0]
+ minute = time.split(':')[1]
+ datetime_str = '%s %s %s %s:%s:%s' % (day, month, self.lp_year,
+ hour.zfill(2), minute.zfill(2),
+ '00')
+
+ dt_object = datetime.strptime(datetime_str, datetime_fmt)
+
+ return vDatetime(eastern.localize(dt_object))
+
+
+ def mk_attendee(self, speaker):
+ """
+ Make Attendee to be added to an Event object.
+
+ See `add_event` method.
+ """
+ # Get rid of HTML (<a> element, etc) in `speaker`
+ speaker = BeautifulSoup(speaker, 'html.parser').get_text()
+
+ attendee = vCalAddress('invalid:nomail')
+ attendee.params['cn'] = vText(speaker)
+ attendee.params['ROLE'] = vText('REQ-PARTICIPANT')
+ attendee.params['CUTYPE'] = vText('INDIVIDUAL')
+
+ return attendee
+
+
+ def add_event(self, month, day, t_start, t_end, session, session_info):
+ """Adds event to calendar.
+ """
+ event = Event()
+ event['uid'] = self.gen_uid()
+ event['dtstamp'] = self.dtstamp
+ event['class'] = vText('PUBLIC')
+ event['status'] = vText('CONFIRMED')
+ event['method'] = vText('PUBLISH')
+
+ event['summary'] = session
+ event['location'] = vText(session_info['room'])
+
+ # Get rid of HTML in 'desc'
+ desc = BeautifulSoup(' '.join(
+ session_info['desc']).replace(
+ '\n', ' '), 'html.parser').get_text()
+ event['description'] = desc
+
+ # Add speakers
+ for speaker in session_info['speakers']:
+ event.add('attendee', self.mk_attendee(speaker), encode=0)
+
+ dt_start = self.mk_datetime(month, day, t_start)
+ dt_end = self.mk_datetime(month, day, t_end)
+
+ event['dtstart'] = dt_start
+ event['dtend'] = dt_end
+
+ # Add to calendar
+ self.cal.add_component(event)
+
+ return event
+
+
+ def gen_ical(self):
+ """Parse LP schedule dict and generate iCal Calendar object.
+
+ """
+
+ for day_str, timeslots in self.lps_dict.iteritems():
+ month, day = self.get_month_day(day_str)
+ for timeslot_str, sessions in timeslots.iteritems():
+ t_start, t_end, t_name = self.get_timeslot(timeslot_str)
+ for session, session_info in sessions.iteritems():
+ self.add_event(month, day, t_start, t_end,
+ session, session_info)
+
+ return self.cal.to_ical()
+
+
+ def to_ical(self):
+ """Writes iCal to disk.
+
+ """
+ filename = 'lp%s-schedule.ics' % self.lp_year
+ write_file(filename, self.gen_ical())
+
+ return filename
+
+
class LPSRenderer(Renderer):
"""Helps in converting Markdown version of LP schedule to a dictionary.
"""
diff --git a/setup.py b/setup.py
index 2074b1d..3cb27ae 100644
--- a/setup.py
+++ b/setup.py
@@ -43,7 +43,7 @@ config = {
'url': 'https://notabug.org/rsd/lpschedule-generator/',
'author': 'rsiddharth',
'author_email': 'rsd@gnu.org',
- 'install_requires': ['mistune', 'Jinja2', 'beautifulsoup4', 'unidecode'],
+ 'install_requires': ['mistune', 'Jinja2', 'beautifulsoup4', 'unidecode', 'icalendar', 'pytz'],
'tests_require': ['nose', 'mock'],
'test_suite': 'nose.collector',
'py_modules': ['lps_gen', 'lpschedule_generator._version'],
diff --git a/tests/test_lps_gen.py b/tests/test_lps_gen.py
index 9cc9406..3d28101 100644
--- a/tests/test_lps_gen.py
+++ b/tests/test_lps_gen.py
@@ -29,7 +29,10 @@ from collections import OrderedDict
from os import path
from StringIO import StringIO
+from bs4 import BeautifulSoup
+from icalendar import vCalAddress, vText, vDatetime
from nose.tools import *
+from pytz import timezone
from lps_gen import *
@@ -119,7 +122,38 @@ class TestLPiCal(object):
def setup_class(self):
"""Setting up things for Testing LPiCal class.
"""
- self.lp_ical = LPiCal({})
+
+ # Change current working directory to the tests directory.
+ self.old_cwd = os.getcwd()
+ os.chdir('tests')
+
+ self.MD_FILE = path.join('files', 'lp-sch.md')
+ self.MD_FILE_CONTENT = read_file(self.MD_FILE)
+
+ self.SCH_TEMPLATE = path.join('..', 'libreplanet-templates/2016',
+ 'lp-schedule.jinja2')
+
+ self.markdown = LPSMarkdown()
+ self.lps_dict = self.markdown(self.MD_FILE_CONTENT)
+ self.purge_list = ['speakers.noids']
+
+
+ def setup(self):
+ """Setting up things for a new test.
+ """
+ self.lp_ical = LPiCal(self.lps_dict, '2016')
+
+
+ def test_gen_uid(self):
+ """Testing LPiCal.gen_uid.
+ """
+
+ uid_fmt = ''.join(['{id}@LP', self.lp_ical.lp_year,
+ '@libreplanet.org'])
+
+ for i in range(40):
+ assert_equals(self.lp_ical.gen_uid(),
+ uid_fmt.format(id=i+1))
def test_get_timeslot(self):
@@ -128,20 +162,29 @@ class TestLPiCal(object):
"""
timeslots = {
- '09:00-09:45: Registration and Breakfast': ['09:00', '09:45'],
- ' 09:45 - 10:45: Opening Keynote': ['09:45', '10:45'],
- '10:5 - 10:55: Break': ['10:5', '10:55'],
- ' 10:55 - 11:40: Session Block 1A': ['10:55', '11:40'],
- ' 11:40 - 11:50: Break': ['11:40', '11:50'],
- '9:45 - 10:30: Keynote ': ['9:45', '10:30'],
- '16:55 - 17:40:Session Block 6B': ['16:55', '17:40'],
- '17:50 - 18:35: Closing keynote': ['17:50', '18:35'],
+ '09:00-09:45: Registration and Breakfast':
+ ['09:00', '09:45', 'Registration and Breakfast'],
+ ' 09:45 - 10:45: Opening Keynote':
+ ['09:45', '10:45', 'Opening Keynote'],
+ '10:5 - 10:55: Break':
+ ['10:5', '10:55', 'Break'],
+ ' 10:55 - 11:40: Session Block 1A':
+ ['10:55', '11:40', 'Session Block 1A'],
+ ' 11:40 - 11:50: Break':
+ ['11:40', '11:50', 'Break'],
+ '9:45 - 10:30: Keynote ':
+ ['9:45', '10:30', 'Keynote'],
+ '16:55 - 17:40:Session Block 6B':
+ ['16:55', '17:40', 'Session Block 6B'],
+ '17:50 - 18:35: Closing keynote':
+ ['17:50', '18:35', 'Closing keynote'],
}
for string, timeslot in timeslots.iteritems():
- start, end = self.lp_ical.get_timeslot(string)
+ start, end, name = self.lp_ical.get_timeslot(string)
assert_equal(start, timeslot[0])
assert_equal(end, timeslot[1])
+ assert_equal(name, timeslot[2])
def test_get_month_day(self):
@@ -163,12 +206,116 @@ class TestLPiCal(object):
assert_equal(day, month_day[1])
+ def test_mk_datetime(self):
+ """Testing LPiCal.mk_datetime
+ """
+
+ datetimes = [
+ {
+ 'params': ['February', '28','08:00'],
+ 'datetime': '2016-02-28 08:00:00',
+ },
+ {
+ 'params': ['March', '21', '9:0'],
+ 'datetime': '2016-03-21 09:00:00',
+ },
+ {
+ 'params': ['March', '23', '15:30'],
+ 'datetime': '2016-03-23 15:30:00',
+ },
+ ]
+
+ for test in datetimes:
+ month = test['params'][0]
+ day = test['params'][1]
+ time = test['params'][2]
+
+ dt_obj = self.lp_ical.mk_datetime(month, day, time)
+
+ assert str(dt_obj.dt.tzinfo) == 'US/Eastern'
+ assert str(dt_obj.dt)[:-6] == test['datetime']
+
+
+ def test_mk_attendee(self):
+ """Testing LPiCal.mk_attendee
+ """
+ speakers = [
+ 'Richard Stallman',
+ 'ginger coons',
+ '<a href="speakers.htmll#corvellec">Marianne Corvellec</a>',
+ '<a href="speakers.html#le-lous">Jonathan Le Lous</a>',
+ 'Jonas \xc3\x96berg',
+ ]
+
+ for speaker in speakers:
+ attendee = self.lp_ical.mk_attendee(speaker)
+ assert str(attendee) == 'invalid:nomail'
+ assert attendee.params.get('cn') == BeautifulSoup(
+ speaker, 'html.parser').get_text()
+ assert attendee.params.get('ROLE') == 'REQ-PARTICIPANT'
+ assert attendee.params.get('CUTYPE') == 'INDIVIDUAL'
+
+
+ def test_add_event(self):
+ """Testing LPiCal.add_event
+ """
+ uids = []
+
+ for day_str, timeslots in self.lps_dict.iteritems():
+ month, day = self.lp_ical.get_month_day(day_str)
+ for timeslot_str, sessions in timeslots.iteritems():
+ t_start, t_end, t_name = self.lp_ical.get_timeslot(timeslot_str)
+ for session, session_info in sessions.iteritems():
+ event = self.lp_ical.add_event(month, day, t_start, t_end,
+ session, session_info)
+ assert event['uid'] not in uids
+ uids.append(event['uid'])
+
+ assert event['dtstamp'] == self.lp_ical.dtstamp
+ assert event['class'] == 'PUBLIC'
+ assert event['status'] == 'CONFIRMED'
+ assert event['method'] == 'PUBLISH'
+ assert event['summary'] == session
+ assert event['location'] == session_info['room']
+ assert event['description'] == BeautifulSoup(' '.join(
+ session_info['desc']).replace(
+ '\n',' '), 'html.parser').get_text()
+
+ if type(event['attendee']) is list:
+ for attendee in event['attendee']:
+ assert isinstance(attendee, vCalAddress)
+ else:
+ assert isinstance(event['attendee'], vCalAddress)
+
+ assert isinstance(event['dtstart'], vDatetime)
+ assert isinstance(event['dtend'], vDatetime)
+
+
+ def test_gen_ical(self):
+ """Testing LPiCal.gen_ical.
+ """
+ ical = self.lp_ical.gen_ical()
+
+
+ def test_to_ical(self):
+ """Testing LPiCal.to_ical.
+ """
+ self.purge_list.append(self.lp_ical.to_ical())
+
+
@classmethod
def teardown_class(self):
"""
Tearing down the mess created by Testing LPiCal class.
"""
- pass
+
+ # remove files in the purge_list.
+ for f in self.purge_list:
+ if path.isfile(f):
+ os.remove(f)
+
+ # Change back to the old cwd
+ os.chdir(self.old_cwd)
class TestLPS(object):
@@ -349,6 +496,7 @@ class TestLPS(object):
# Change back to the old cwd
os.chdir(self.old_cwd)
+
class TestLPSpeakers(object):
"""
Class that tests everything related LP Speakers