1
0
Fork 0
sito-hackit-17/plugins/talks.py

395 lines
13 KiB
Python
Raw Permalink Normal View History

2017-05-03 01:43:18 +02:00
'''
Manage talks scheduling in a semantic way
'''
2017-05-24 11:17:23 +02:00
2017-05-03 01:43:18 +02:00
from __future__ import print_function
import os
import io
from functools import wraps
import logging
import re
import datetime
import shutil
2017-05-06 02:00:42 +02:00
import time
2017-05-07 02:18:04 +02:00
from copy import copy
2017-05-18 19:24:39 +02:00
import locale
from contextlib import contextmanager
from babel.dates import format_date, format_datetime, format_time
2017-05-03 01:43:18 +02:00
2017-05-07 02:18:04 +02:00
import markdown
2017-05-03 01:43:18 +02:00
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from pelican import signals, generators
import jinja2
2017-05-24 11:17:23 +02:00
pelican = None # This will be set during register()
2017-05-03 01:43:18 +02:00
def memoize(function):
'''decorators to cache'''
memo = {}
@wraps(function)
def wrapper(*args):
if args in memo:
return memo[args]
else:
rv = function(*args)
memo[args] = rv
return rv
return wrapper
2017-05-18 19:24:39 +02:00
@contextmanager
def setlocale(name):
saved = locale.setlocale(locale.LC_ALL)
try:
yield locale.setlocale(locale.LC_ALL, name)
finally:
locale.setlocale(locale.LC_ALL, saved)
2017-05-03 01:43:18 +02:00
@memoize
def get_talk_names():
2017-05-24 11:17:23 +02:00
return [name for name in os.listdir(pelican.settings['TALKS_PATH'])
2017-05-03 01:43:18 +02:00
if not name.startswith('_') and
get_talk_data(name) is not None
]
2017-05-06 02:00:42 +02:00
def all_talks():
return [get_talk_data(tn) for tn in get_talk_names()]
2017-05-06 03:24:17 +02:00
def unique_attr(iterable, attr):
return {x[attr] for x in iterable
if attr in x}
2017-05-06 02:00:42 +02:00
@memoize
def get_global_data():
2017-05-24 11:17:23 +02:00
fname = os.path.join(pelican.settings['TALKS_PATH'], 'meta.yaml')
2017-05-06 02:00:42 +02:00
if not os.path.isfile(fname):
return None
with io.open(fname, encoding='utf8') as buf:
try:
data = yaml.load(buf)
2017-05-07 01:07:08 +02:00
except Exception:
2017-05-06 02:00:42 +02:00
logging.exception("Syntax error reading %s; skipping", fname)
return None
if data is None:
return None
if 'startdate' not in data:
logging.error("Missing startdate in global data")
data['startdate'] = datetime.datetime.now()
return data
2017-05-03 01:43:18 +02:00
@memoize
def get_talk_data(talkname):
2017-05-24 11:17:23 +02:00
fname = os.path.join(pelican.settings['TALKS_PATH'], talkname, 'meta.yaml')
2017-05-03 01:43:18 +02:00
if not os.path.isfile(fname):
return None
with io.open(fname, encoding='utf8') as buf:
2017-05-05 17:12:38 +02:00
try:
data = yaml.load(buf)
2017-05-22 22:59:01 +02:00
except:
2017-05-05 17:12:38 +02:00
logging.exception("Syntax error reading %s; skipping", fname)
return None
2017-05-03 01:43:18 +02:00
if data is None:
return None
2017-05-22 11:13:25 +02:00
try:
2017-05-24 11:17:23 +02:00
gridstep = pelican.settings['TALKS_GRID_STEP']
2017-05-22 11:13:25 +02:00
if 'title' not in data:
logging.warn("Talk <{}> has no `title` field".format(talkname))
data['title'] = talkname
if 'text' not in data:
logging.warn("Talk <{}> has no `text` field".format(talkname))
data['text'] = ''
if 'duration' not in data:
logging.info("Talk <{}> has no `duration` field (50min used)"
.format(talkname))
data['duration'] = 50
data['duration'] = int(data['duration'])
2017-05-24 11:17:23 +02:00
if data['duration'] < gridstep:
2017-05-22 11:13:25 +02:00
logging.info("Talk <{}> lasts only {} minutes; changing to {}"
2017-05-24 11:17:23 +02:00
.format(talkname, data['duration'], gridstep))
data['duration'] = gridstep
2017-05-22 11:13:25 +02:00
if 'links' not in data or not data['links']:
data['links'] = []
if 'contacts' not in data or not data['contacts']:
data['contacts'] = []
if 'needs' not in data or not data['needs']:
data['needs'] = []
if 'room' not in data:
logging.warn("Talk <{}> has no `room` field".format(talkname))
if 'time' not in data or 'day' not in data:
logging.warn("Talk <{}> has no `time` or `day`".format(talkname))
if 'time' in data:
del data['time']
if 'day' in data:
del data['day']
if 'day' in data:
2017-05-22 22:59:01 +02:00
data['day'] = get_global_data()['startdate'] + \
datetime.timedelta(days=data['day'])
2017-05-22 11:13:25 +02:00
if 'time' in data and 'day' in data:
timeparts = re.findall(r'\d+', str(data['time']))
if 4 > len(timeparts) > 0:
timeparts = [int(p) for p in timeparts]
2017-05-22 22:59:01 +02:00
data['time'] = datetime.datetime.combine(
data['day'], datetime.time(*timeparts))
2017-05-22 11:13:25 +02:00
else:
2017-05-22 22:59:01 +02:00
logging.error("Talk <%s> has malformed `time`", talkname)
2017-05-22 11:13:25 +02:00
data['id'] = talkname
2017-05-24 11:17:23 +02:00
resdir = os.path.join(pelican.settings['TALKS_PATH'], talkname,
pelican.settings['TALKS_ATTACHMENT_PATH'])
2017-05-22 11:13:25 +02:00
if os.path.isdir(resdir) and os.listdir(resdir):
data['resources'] = resdir
return data
except:
logging.exception("Error on talk %s", talkname)
raise
2017-05-03 01:43:18 +02:00
2017-05-24 11:17:23 +02:00
@memoize
def jinja_env():
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.join(pelican.settings['TALKS_PATH'], '_templates')),
autoescape=True,
)
env.filters['markdown'] = lambda text: \
jinja2.Markup(markdown.Markdown(extensions=['meta']).
convert(text))
env.filters['dateformat'] = format_date
env.filters['datetimeformat'] = format_datetime
env.filters['timeformat'] = format_time
return env
2017-05-03 01:43:18 +02:00
class TalkListDirective(Directive):
final_argument_whitespace = True
has_content = True
2017-05-18 19:24:39 +02:00
option_spec = {
'lang': directives.unchanged
}
2017-05-03 01:43:18 +02:00
def run(self):
2017-05-18 19:24:39 +02:00
lang = self.options.get('lang', 'C')
2017-05-24 11:17:23 +02:00
tmpl = jinja_env().get_template('talk.html')
2017-06-09 11:42:08 +02:00
def _sort_date(name):
'''
This function is a helper to sort talks by start date
When no date is available, put at the beginning
'''
d = get_talk_data(name)
if 'time' in d:
return d['time']
return datetime.datetime(1, 1, 1)
2017-05-03 01:43:18 +02:00
return [
2017-05-18 19:24:39 +02:00
nodes.raw('', tmpl.render(lang=lang, **get_talk_data(n)),
2017-05-03 01:43:18 +02:00
format='html')
2017-06-09 11:42:08 +02:00
for n in sorted(get_talk_names(),
key=_sort_date)
]
2017-05-03 01:43:18 +02:00
2017-05-03 02:23:56 +02:00
class TalkDirective(Directive):
required_arguments = 1
final_argument_whitespace = True
has_content = True
2017-05-18 19:24:39 +02:00
option_spec = {
'lang': directives.unchanged
}
2017-05-03 02:23:56 +02:00
def run(self):
2017-05-18 19:24:39 +02:00
lang = self.options.get('lang', 'C')
2017-05-24 11:17:23 +02:00
tmpl = jinja_env().get_template('talk.html')
2017-05-03 02:23:56 +02:00
data = get_talk_data(self.arguments[0])
if data is None:
return []
return [
2017-05-18 19:24:39 +02:00
nodes.raw('', tmpl.render(lang=lang, **data),
2017-05-03 02:23:56 +02:00
format='html')
]
2017-05-06 02:00:42 +02:00
2017-05-06 03:24:17 +02:00
class TalkGridDirective(Directive):
'''A complete grid'''
final_argument_whitespace = True
has_content = True
2017-05-18 19:24:39 +02:00
option_spec = {
'lang': directives.unchanged
}
2017-05-06 03:24:17 +02:00
def run(self):
2017-05-18 19:24:39 +02:00
lang = self.options.get('lang', 'C')
2017-05-24 11:17:23 +02:00
tmpl = jinja_env().get_template('grid.html')
2017-05-06 03:24:17 +02:00
output = []
days = unique_attr(all_talks(), 'day')
2017-05-24 11:17:23 +02:00
gridstep = pelican.settings['TALKS_GRID_STEP']
2017-05-06 03:24:17 +02:00
for day in sorted(days):
talks = {talk['id'] for talk in all_talks()
if talk.get('day', None) == day
and 'time' in talk
and 'room' in talk}
if not talks:
continue
talks = [get_talk_data(t) for t in talks]
rooms = set()
for t in talks:
if type(t['room']) is list:
for r in t['room']:
rooms.add(r)
else:
rooms.add(t['room'])
rooms = list(sorted(rooms))
# room=* is not a real room.
# Remove it unless that day only has special rooms
if '*' in rooms and len(rooms) > 1:
del rooms[rooms.index('*')]
2017-05-06 03:24:17 +02:00
mintime = min({talk['time'].hour * 60 +
talk['time'].minute
2017-05-24 11:17:23 +02:00
for talk in talks}) // gridstep * gridstep
2017-05-06 03:24:17 +02:00
maxtime = max({talk['time'].hour * 60 +
talk['time'].minute +
talk['duration']
for talk in talks})
times = {}
2017-05-24 11:17:23 +02:00
for t in range(mintime, maxtime, gridstep):
2017-05-06 03:24:17 +02:00
times[t] = [None] * len(rooms)
2017-05-07 02:18:04 +02:00
for talk in sorted(talks, key=lambda x: x['time']):
2017-05-06 03:24:17 +02:00
talktime = talk['time'].hour * 60 + talk['time'].minute
2017-05-24 11:17:23 +02:00
position = talktime // gridstep * gridstep # round
2017-05-06 03:24:17 +02:00
assert position in times
if talk['room'] == '*':
roomnums = range(len(rooms))
elif type(talk['room']) is list:
roomnums = [rooms.index(r) for r in talk['room']]
else:
roomnums = [rooms.index(talk['room'])]
for roomnum in roomnums:
if times[position][roomnum] is not None:
logging.error("Talk %s and %s overlap! (room %s)",
times[position][roomnum]['id'],
talk['id'],
rooms[roomnum]
)
continue
times[position][roomnum] = copy(talk)
times[position][roomnum]['skip'] = False
2017-05-24 11:17:23 +02:00
for i in range(1, talk['duration'] // gridstep):
times[position + i*gridstep][roomnum] = copy(talk)
times[position + i*gridstep][roomnum]['skip'] = True
2017-05-06 03:24:17 +02:00
render = tmpl.render(times=times,
rooms=rooms,
mintime=mintime, maxtime=maxtime,
2017-05-24 11:17:23 +02:00
timestep=gridstep,
2017-05-18 19:24:39 +02:00
lang=lang,
2017-05-06 03:24:17 +02:00
)
2017-05-22 22:59:01 +02:00
output.append(nodes.raw(
'', u'<h4>%s</h4>' % format_date(day, format='full',
locale=lang),
format='html'))
2017-05-06 03:24:17 +02:00
output.append(nodes.raw('', render, format='html'))
return output
2017-05-06 02:00:42 +02:00
def talks_to_ics():
2017-05-21 00:25:10 +02:00
content = u'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:pelican\n'
2017-05-06 02:00:42 +02:00
for t in all_talks():
2017-05-22 11:13:25 +02:00
try:
content += talk_to_ics(t)
except:
logging.exception("Error producing calendar for talk %s", t['id'])
2017-05-06 02:00:42 +02:00
content += 'END:VCALENDAR\n'
return content
def talk_to_ics(talk):
2017-06-10 00:40:57 +02:00
if 'time' not in talk or 'duration' not in talk:
2017-05-06 03:24:17 +02:00
return ''
2017-05-06 02:00:42 +02:00
start = talk['time']
2017-05-22 22:59:01 +02:00
end = start + datetime.timedelta(minutes=talk['duration'])
2017-05-06 02:00:42 +02:00
content = 'BEGIN:VEVENT\n'
content += "UID:%s@%d.hackmeeting.org\n" % (talk['id'], talk['day'].year)
content += "SUMMARY:%s\n" % talk['title']
content += "DTSTAMP:%s\n" % time.strftime('%Y%m%dT%H%M%SZ',
2017-05-22 22:59:01 +02:00
time.gmtime(float(
start.strftime('%s'))))
2017-05-06 02:00:42 +02:00
content += "DTSTART:%s\n" % time.strftime('%Y%m%dT%H%M%SZ',
time.gmtime(float(
start.strftime('%s'))))
content += "DTEND:%s\n" % time.strftime('%Y%m%dT%H%M%SZ',
2017-05-22 22:59:01 +02:00
time.gmtime(float(
2017-05-06 02:00:42 +02:00
end.strftime('%s'))))
2017-06-10 00:40:57 +02:00
content += "LOCATION:%s\n" % (talk['room'] if 'room' in talk else 'todo')
2017-05-06 02:00:42 +02:00
content += 'END:VEVENT\n'
return content
2017-05-03 02:23:56 +02:00
2017-05-03 01:43:18 +02:00
class TalksGenerator(generators.Generator):
def __init__(self, *args, **kwargs):
self.talks = []
super(TalksGenerator, self).__init__(*args, **kwargs)
def generate_context(self):
self.talks = {n: get_talk_data(n) for n in get_talk_names()}
self._update_context(('talks',))
def generate_output(self, writer=None):
for talkname in self.talks:
if 'resources' in self.talks[talkname]:
2017-05-24 11:17:23 +02:00
outdir = os.path.join(self.output_path,
pelican.settings['TALKS_PATH'], talkname,
pelican.settings['TALKS_ATTACHMENT_PATH'])
if os.path.isdir(outdir):
shutil.rmtree(outdir)
shutil.copytree(self.talks[talkname]['resources'], outdir)
2017-05-24 11:17:23 +02:00
with io.open(os.path.join(self.output_path, pelican.settings.get('TALKS_ICS')),
2017-05-22 10:41:43 +02:00
'w',
encoding='utf8') as buf:
buf.write(talks_to_ics())
2017-05-03 01:43:18 +02:00
def add_talks_option_defaults(pelican):
2017-05-24 11:17:23 +02:00
pelican.settings.setdefault('TALKS_PATH', 'talks')
pelican.settings.setdefault('TALKS_ATTACHMENT_PATH', 'res')
pelican.settings.setdefault('TALKS_ICS', 'schedule.ics')
pelican.settings.setdefault('TALKS_GRID_STEP', 30)
2017-05-03 01:43:18 +02:00
def get_generators(gen):
return TalksGenerator
2017-05-24 11:17:23 +02:00
def pelican_init(pelicanobj):
global pelican
pelican = pelicanobj
2017-05-03 01:43:18 +02:00
try:
import yaml
except ImportError:
print('ERROR: yaml not found. Talks plugins will be disabled')
def register():
pass
else:
def register():
2017-05-24 11:17:23 +02:00
signals.initialized.connect(pelican_init)
signals.get_generators.connect(get_generators)
2017-05-03 01:43:18 +02:00
signals.initialized.connect(add_talks_option_defaults)
directives.register_directive('talklist', TalkListDirective)
2017-05-03 02:23:56 +02:00
directives.register_directive('talk', TalkDirective)
2017-05-06 03:24:17 +02:00
directives.register_directive('talkgrid', TalkGridDirective)