diff --git a/plugins/talks.py b/plugins/talks.py deleted file mode 100644 index 01bfa60..0000000 --- a/plugins/talks.py +++ /dev/null @@ -1,521 +0,0 @@ -# -*- coding: utf-8 -*- -''' -Manage talks scheduling in a semantic way -''' - - -from __future__ import print_function -import os -import io -from functools import wraps -import logging -import re -import datetime -import shutil -from copy import copy -import locale -from contextlib import contextmanager -from babel.dates import format_date, format_datetime, format_time - -from markdown import markdown -from docutils import nodes -from docutils.parsers.rst import directives, Directive -import six - -from pelican import signals, generators -import jinja2 - -try: - import ics -except ImportError: - ICS_ENABLED = False -else: - ICS_ENABLED = True -import unidecode -import dateutil - -pelican = None # This will be set during register() - - -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 - - -@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) - - -@memoize -def get_talk_names(): - names = [name for name in os.listdir(pelican.settings['TALKS_PATH']) - if not name.startswith('_') and - get_talk_data(name) is not None - ] - names.sort() - return names - - -def all_talks(): - return [get_talk_data(tn) for tn in get_talk_names()] - - -def unique_attr(iterable, attr): - return {x[attr] for x in iterable - if attr in x} - - -@memoize -def get_global_data(): - fname = os.path.join(pelican.settings['TALKS_PATH'], 'meta.yaml') - if not os.path.isfile(fname): - return None - with io.open(fname, encoding='utf8') as buf: - try: - data = yaml.load(buf) - except Exception: - 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() - if 'rooms' not in data: - data['rooms'] = {} - return data - - -def _get_time_shift(timestring): - ''' Il problema che abbiamo è che vogliamo dire che le 2 di notte del sabato sono in realtà "parte" del - venerdì. Per farlo accettiamo orari che superano le 24, ad esempio 25.30 vuol dire 1.30. - - Questa funzione ritorna una timedelta in base alla stringa passata - ''' - timeparts = re.findall(r'\d+', timestring) - if not timeparts or len(timeparts) > 2: - raise ValueError("Malformed time %s" % timestring) - timeparts += [0,0] # "padding" per essere sicuro ci siano anche [1] e [2] - duration = datetime.timedelta(hours=int(timeparts[0]), - minutes=int(timeparts[1]), - seconds=int(timeparts[2])) - if duration.total_seconds() > 3600 * 31 or duration.total_seconds() < 0: - raise ValueError("Sforamento eccessivo: %d" % duration.hours) - return duration - - - -@memoize -def get_talk_data(talkname): - fname = os.path.join(pelican.settings['TALKS_PATH'], talkname, 'meta.yaml') - if not os.path.isfile(fname): - return None - with io.open(fname, encoding='utf8') as buf: - try: - data = yaml.load(buf) - except Exception: - logging.exception("Syntax error reading %s; skipping", fname) - return None - if data is None: - return None - try: - gridstep = pelican.settings['TALKS_GRID_STEP'] - data.setdefault('nooverlap', []) - if 'title' not in data: - logging.warn("Talk <{}> has no `title` field".format(talkname)) - data['title'] = six.text_type(talkname) - else: - data['title'] = six.text_type(data['title']) - if 'text' not in data: - logging.warn("Talk <{}> has no `text` field".format(talkname)) - data['text'] = '' - else: - data['text'] = six.text_type(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']) - if data['duration'] < gridstep: - logging.info("Talk <{}> lasts only {} minutes; changing to {}" - .format(talkname, data['duration'], gridstep)) - data['duration'] = gridstep - 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)) - else: - if data['room'] in get_global_data()['rooms']: - data['room'] = get_global_data()['rooms'][data['room']] - 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'] - else: - data['day'] = get_global_data()['startdate'] + \ - datetime.timedelta(days=data['day']) - try: - shift = _get_time_shift(str(data['time'])) - except ValueError: - logging.error("Talk <%s> has malformed `time`", talkname) - - data['delta'] = shift - data['time'] = datetime.datetime.combine( - data['day'], datetime.time(0,0,0)) - data['time'] += shift - data['time'] = data['time'].replace(tzinfo=dateutil.tz.gettz('Europe/Rome')) - - - data['id'] = talkname - resdir = os.path.join(pelican.settings['TALKS_PATH'], talkname, - pelican.settings['TALKS_ATTACHMENT_PATH']) - if os.path.isdir(resdir) and os.listdir(resdir): - data['resources'] = resdir - return data - except Exception: - logging.exception("Error on talk %s", talkname) - raise - - -def overlap(interval_a, interval_b): - '''how many minutes do they overlap?''' - return max(0, min(interval_a[1], interval_b[1]) - - max(interval_a[0], interval_b[0])) - - -def get_talk_overlaps(name): - data = get_talk_data(name) - overlapping_talks = set() - if 'time' not in data: - return overlapping_talks - start = int(data['time'].strftime('%s')) - end = start + data['duration'] * 60 - for other in get_talk_names(): - if other == name: - continue - if 'time' not in get_talk_data(other): - continue - other_start = int(get_talk_data(other)['time'].strftime('%s')) - other_end = other_start + get_talk_data(other)['duration'] * 60 - - minutes = overlap((start, end), (other_start, other_end)) - if minutes > 0: - overlapping_talks.add(other) - return overlapping_talks - - -@memoize -def check_overlaps(): - for t in get_talk_names(): - over = get_talk_overlaps(t) - noover = get_talk_data(t)['nooverlap'] - contacts = set(get_talk_data(t)['contacts']) - for overlapping in over: - if overlapping in noover or \ - set(get_talk_data(overlapping)['contacts']).\ - intersection(contacts): - logging.warning('Talk %s overlaps with %s' % (t, overlapping)) - - -@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(text)) - env.filters['dateformat'] = format_date - env.filters['datetimeformat'] = format_datetime - env.filters['timeformat'] = format_time - return env - - -class TalkListDirective(Directive): - final_argument_whitespace = True - has_content = True - option_spec = { - 'lang': directives.unchanged - } - - def run(self): - lang = self.options.get('lang', 'C') - tmpl = jinja_env().get_template('talk.html') - - 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) - room = d.get('room', '') - time = d.get('time', datetime.datetime(1, 1, 1).replace( - tzinfo=dateutil.tz.gettz('Europe/Rome'))) - title = d.get('title', '') - return (time, room, title) - - return [ - nodes.raw('', tmpl.render(lang=lang, **get_talk_data(n)), - format='html') - for n in sorted(get_talk_names(), - key=_sort_date) - ] - - -class TalkDirective(Directive): - required_arguments = 1 - final_argument_whitespace = True - has_content = True - option_spec = { - 'lang': directives.unchanged - } - - def run(self): - lang = self.options.get('lang', 'C') - tmpl = jinja_env().get_template('talk.html') - data = get_talk_data(self.arguments[0]) - if data is None: - return [] - return [ - nodes.raw('', tmpl.render(lang=lang, **data), - format='html') - ] - - -def _delta_to_position(delta): - gridstep = pelican.settings['TALKS_GRID_STEP'] - sec = delta.total_seconds() // gridstep * gridstep - return int('%2d%02d' % (sec // 3600, (sec % 3600) // 60)) - - -def _delta_inc_position(delta, i): - gridstep = pelican.settings['TALKS_GRID_STEP'] - delta = delta + datetime.timedelta(minutes=i*gridstep) - sec = delta.total_seconds() // gridstep * gridstep - return int('%2d%02d' % (sec // 3600, (sec % 3600) // 60)) - -def _approx_timestr(timestr): - gridstep = pelican.settings['TALKS_GRID_STEP'] - t = str(timestr) - minutes = int(t[-2:]) - hours = t[:-2] - minutes = minutes // gridstep * gridstep - return int('%s%02d' % (hours, minutes)) - - -class TalkGridDirective(Directive): - '''A complete grid''' - final_argument_whitespace = True - has_content = True - option_spec = { - 'lang': directives.unchanged - } - - def run(self): - try: - lang = self.options.get('lang', 'C') - tmpl = jinja_env().get_template('grid.html') - output = [] - days = unique_attr(all_talks(), 'day') - gridstep = pelican.settings['TALKS_GRID_STEP'] - 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']) - # TODO: ordina in base a qualcosa nel meta.yaml globale - 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('*')] - mintimedelta = min({talk['delta'] for talk in talks}) - maxtimedelta = max({talk['delta'] + datetime.timedelta(minutes=talk['duration']) for talk in talks}) - mintime = _delta_to_position(mintimedelta) - maxtime = _delta_to_position(maxtimedelta) - times = {} - - t = mintimedelta - while t <= maxtimedelta: - times[_delta_to_position(t)] = [None] * len(rooms) - t += datetime.timedelta(minutes=gridstep) - for talk in sorted(talks, key=lambda x: x['delta']): - talktime = _delta_to_position(talk['delta']) - position = _approx_timestr(talktime) - assert position in times, 'pos=%d,time=%d' % (position, talktime) - 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 - for i in range(1, talk['duration'] // gridstep): - p = _approx_timestr(_delta_inc_position(talk['delta'], i)) - times[p][roomnum] = copy(talk) - times[p][roomnum]['skip'] = True - - - render = tmpl.render(times=times, - rooms=rooms, - mintime=mintime, maxtime=maxtime, - timestep=gridstep, - lang=lang, - ) - output.append(nodes.raw( - '', u'

%s

' % format_date(day, format='full', - locale=lang), - format='html')) - output.append(nodes.raw('', render, format='html')) - except: - logging.exception("Error on talk grid") - import traceback - traceback.print_exc() - return [] - return output - - -def talks_to_ics(): - c = ics.Calendar() - c.creator = u'pelican' - for t in all_talks(): - e = talk_to_ics(t) - if e is not None: - c.events.add(e) - return six.text_type(c) - - -def talk_to_ics(talk): - def _decode(s): - if six.PY2: - return unidecode.unidecode(s) - else: - return s - - if 'time' not in talk or 'duration' not in talk: - return None - e = ics.Event( - uid="%s@%d.hackmeeting.org" % (talk['id'], - get_global_data()['startdate'].year), - name=_decode(talk['title']), - begin=talk['time'], - duration=datetime.timedelta(minutes=talk['duration']), - transparent=True, - ) - # ics.py has some problems with unicode - # unidecode replaces letters with their most similar ASCII counterparts - # (ie: accents get stripped) - if 'text' in talk: - e.description = _decode(talk['text']) - e.url = pelican.settings['SCHEDULEURL'] + '#talk-' + talk['id'] - if 'room' in talk: - e.location = talk['room'] - - return e - - -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',)) - check_overlaps() - - def generate_output(self, writer=None): - for talkname in sorted(self.talks): - if 'resources' in self.talks[talkname]: - 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) - if ICS_ENABLED: - with io.open(os.path.join(self.output_path, - pelican.settings.get('TALKS_ICS')), - 'w', encoding='utf8') as buf: - buf.write(talks_to_ics()) - else: - logging.warning('module `ics` not found. ' - 'ICS calendar will not be generated') - - -def add_talks_option_defaults(pelican): - 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) - - -def get_generators(gen): - return TalksGenerator - - -def pelican_init(pelicanobj): - global pelican - pelican = pelicanobj - - -try: - import yaml -except ImportError: - print('ERROR: yaml not found. Talks plugins will be disabled') - - def register(): - pass -else: - - def register(): - signals.initialized.connect(pelican_init) - signals.get_generators.connect(get_generators) - signals.initialized.connect(add_talks_option_defaults) - directives.register_directive('talklist', TalkListDirective) - directives.register_directive('talk', TalkDirective) - directives.register_directive('talkgrid', TalkGridDirective) diff --git a/plugins/talks/__init__.py b/plugins/talks/__init__.py new file mode 100644 index 0000000..64e7f13 --- /dev/null +++ b/plugins/talks/__init__.py @@ -0,0 +1,3 @@ +from .talks import * + +# flake8: noqa diff --git a/plugins/talks/talks.py b/plugins/talks/talks.py new file mode 100644 index 0000000..27e01de --- /dev/null +++ b/plugins/talks/talks.py @@ -0,0 +1,543 @@ +# -*- coding: utf-8 -*- +""" +Manage talks scheduling in a semantic way +""" + + +from __future__ import print_function +import os +import io +from functools import wraps +import logging +import re +import datetime +import shutil +from copy import copy +import locale +from contextlib import contextmanager +from babel.dates import format_date, format_datetime, format_time + +from markdown import markdown +from docutils import nodes +from docutils.parsers.rst import directives, Directive +import six + +from pelican import signals, generators +import jinja2 + +try: + import ics +except ImportError: + ICS_ENABLED = False +else: + ICS_ENABLED = True +import unidecode +import dateutil + +pelican = None # This will be set during register() + + +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 + + +@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) + + +@memoize +def get_talk_names(): + names = [ + name + for name in os.listdir(pelican.settings["TALKS_PATH"]) + if not name.startswith("_") and get_talk_data(name) is not None + ] + names.sort() + return names + + +def all_talks(): + return [get_talk_data(tn) for tn in get_talk_names()] + + +def unique_attr(iterable, attr): + return {x[attr] for x in iterable if attr in x} + + +@memoize +def get_global_data(): + fname = os.path.join(pelican.settings["TALKS_PATH"], "meta.yaml") + if not os.path.isfile(fname): + return None + with io.open(fname, encoding="utf8") as buf: + try: + data = yaml.load(buf) + except Exception: + 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() + if "rooms" not in data: + data["rooms"] = {} + return data + + +def _get_time_shift(timestring): + """ Il problema che abbiamo è che vogliamo dire che le 2 di notte del sabato sono in realtà "parte" del + venerdì. Per farlo accettiamo orari che superano le 24, ad esempio 25.30 vuol dire 1.30. + + Questa funzione ritorna una timedelta in base alla stringa passata + """ + timeparts = re.findall(r"\d+", timestring) + if not timeparts or len(timeparts) > 2: + raise ValueError("Malformed time %s" % timestring) + timeparts += [0, 0] # "padding" per essere sicuro ci siano anche [1] e [2] + duration = datetime.timedelta( + hours=int(timeparts[0]), minutes=int(timeparts[1]), seconds=int(timeparts[2]) + ) + if duration.total_seconds() > 3600 * 31 or duration.total_seconds() < 0: + raise ValueError("Sforamento eccessivo: %d" % duration.hours) + return duration + + +@memoize +def get_talk_data(talkname): + fname = os.path.join(pelican.settings["TALKS_PATH"], talkname, "meta.yaml") + if not os.path.isfile(fname): + return None + with io.open(fname, encoding="utf8") as buf: + try: + data = yaml.load(buf) + except Exception: + logging.exception("Syntax error reading %s; skipping", fname) + return None + if data is None: + return None + try: + gridstep = pelican.settings["TALKS_GRID_STEP"] + data.setdefault("nooverlap", []) + if "title" not in data: + logging.warn("Talk <{}> has no `title` field".format(talkname)) + data["title"] = six.text_type(talkname) + else: + data["title"] = six.text_type(data["title"]) + if "text" not in data: + logging.warn("Talk <{}> has no `text` field".format(talkname)) + data["text"] = "" + else: + data["text"] = six.text_type(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"]) + if data["duration"] < gridstep: + logging.info( + "Talk <{}> lasts only {} minutes; changing to {}".format( + talkname, data["duration"], gridstep + ) + ) + data["duration"] = gridstep + 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)) + else: + if data["room"] in get_global_data()["rooms"]: + data["room"] = get_global_data()["rooms"][data["room"]] + 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"] + else: + data["day"] = get_global_data()["startdate"] + datetime.timedelta( + days=data["day"] + ) + try: + shift = _get_time_shift(str(data["time"])) + except ValueError: + logging.error("Talk <%s> has malformed `time`", talkname) + + data["delta"] = shift + data["time"] = datetime.datetime.combine( + data["day"], datetime.time(0, 0, 0) + ) + data["time"] += shift + data["time"] = data["time"].replace(tzinfo=dateutil.tz.gettz("Europe/Rome")) + + data["id"] = talkname + resdir = os.path.join( + pelican.settings["TALKS_PATH"], + talkname, + pelican.settings["TALKS_ATTACHMENT_PATH"], + ) + if os.path.isdir(resdir) and os.listdir(resdir): + data["resources"] = resdir + return data + except Exception: + logging.exception("Error on talk %s", talkname) + raise + + +def overlap(interval_a, interval_b): + """how many minutes do they overlap?""" + return max(0, min(interval_a[1], interval_b[1]) - max(interval_a[0], interval_b[0])) + + +def get_talk_overlaps(name): + data = get_talk_data(name) + overlapping_talks = set() + if "time" not in data: + return overlapping_talks + start = int(data["time"].strftime("%s")) + end = start + data["duration"] * 60 + for other in get_talk_names(): + if other == name: + continue + if "time" not in get_talk_data(other): + continue + other_start = int(get_talk_data(other)["time"].strftime("%s")) + other_end = other_start + get_talk_data(other)["duration"] * 60 + + minutes = overlap((start, end), (other_start, other_end)) + if minutes > 0: + overlapping_talks.add(other) + return overlapping_talks + + +@memoize +def check_overlaps(): + for t in get_talk_names(): + over = get_talk_overlaps(t) + noover = get_talk_data(t)["nooverlap"] + contacts = set(get_talk_data(t)["contacts"]) + for overlapping in over: + if overlapping in noover or set( + get_talk_data(overlapping)["contacts"] + ).intersection(contacts): + logging.warning("Talk %s overlaps with %s" % (t, overlapping)) + + +@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(text)) + env.filters["dateformat"] = format_date + env.filters["datetimeformat"] = format_datetime + env.filters["timeformat"] = format_time + return env + + +class TalkListDirective(Directive): + final_argument_whitespace = True + has_content = True + option_spec = {"lang": directives.unchanged} + + def run(self): + lang = self.options.get("lang", "C") + tmpl = jinja_env().get_template("talk.html") + + 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) + room = d.get("room", "") + time = d.get( + "time", + datetime.datetime(1, 1, 1).replace( + tzinfo=dateutil.tz.gettz("Europe/Rome") + ), + ) + title = d.get("title", "") + return (time, room, title) + + return [ + nodes.raw("", tmpl.render(lang=lang, **get_talk_data(n)), format="html") + for n in sorted(get_talk_names(), key=_sort_date) + ] + + +class TalkDirective(Directive): + required_arguments = 1 + final_argument_whitespace = True + has_content = True + option_spec = {"lang": directives.unchanged} + + def run(self): + lang = self.options.get("lang", "C") + tmpl = jinja_env().get_template("talk.html") + data = get_talk_data(self.arguments[0]) + if data is None: + return [] + return [nodes.raw("", tmpl.render(lang=lang, **data), format="html")] + + +def _delta_to_position(delta): + gridstep = pelican.settings["TALKS_GRID_STEP"] + sec = delta.total_seconds() // gridstep * gridstep + return int("%2d%02d" % (sec // 3600, (sec % 3600) // 60)) + + +def _delta_inc_position(delta, i): + gridstep = pelican.settings["TALKS_GRID_STEP"] + delta = delta + datetime.timedelta(minutes=i * gridstep) + sec = delta.total_seconds() // gridstep * gridstep + return int("%2d%02d" % (sec // 3600, (sec % 3600) // 60)) + + +def _approx_timestr(timestr): + gridstep = pelican.settings["TALKS_GRID_STEP"] + t = str(timestr) + minutes = int(t[-2:]) + hours = t[:-2] + minutes = minutes // gridstep * gridstep + return int("%s%02d" % (hours, minutes)) + + +class TalkGridDirective(Directive): + """A complete grid""" + + final_argument_whitespace = True + has_content = True + option_spec = {"lang": directives.unchanged} + + def run(self): + try: + lang = self.options.get("lang", "C") + tmpl = jinja_env().get_template("grid.html") + output = [] + days = unique_attr(all_talks(), "day") + gridstep = pelican.settings["TALKS_GRID_STEP"] + 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"]) + # TODO: ordina in base a qualcosa nel meta.yaml globale + 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("*")] + mintimedelta = min({talk["delta"] for talk in talks}) + maxtimedelta = max( + { + talk["delta"] + datetime.timedelta(minutes=talk["duration"]) + for talk in talks + } + ) + mintime = _delta_to_position(mintimedelta) + maxtime = _delta_to_position(maxtimedelta) + times = {} + + t = mintimedelta + while t <= maxtimedelta: + times[_delta_to_position(t)] = [None] * len(rooms) + t += datetime.timedelta(minutes=gridstep) + for talk in sorted(talks, key=lambda x: x["delta"]): + talktime = _delta_to_position(talk["delta"]) + position = _approx_timestr(talktime) + assert position in times, "pos=%d,time=%d" % (position, talktime) + 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 + for i in range(1, talk["duration"] // gridstep): + p = _approx_timestr(_delta_inc_position(talk["delta"], i)) + times[p][roomnum] = copy(talk) + times[p][roomnum]["skip"] = True + + render = tmpl.render( + times=times, + rooms=rooms, + mintime=mintime, + maxtime=maxtime, + timestep=gridstep, + lang=lang, + ) + output.append( + nodes.raw( + "", + u"

%s

" % format_date(day, format="full", locale=lang), + format="html", + ) + ) + output.append(nodes.raw("", render, format="html")) + except: + logging.exception("Error on talk grid") + import traceback + + traceback.print_exc() + return [] + return output + + +def talks_to_ics(): + c = ics.Calendar() + c.creator = u"pelican" + for t in all_talks(): + e = talk_to_ics(t) + if e is not None: + c.events.add(e) + return six.text_type(c) + + +def talk_to_ics(talk): + def _decode(s): + if six.PY2: + return unidecode.unidecode(s) + else: + return s + + if "time" not in talk or "duration" not in talk: + return None + e = ics.Event( + uid="%s@%d.hackmeeting.org" % (talk["id"], get_global_data()["startdate"].year), + name=_decode(talk["title"]), + begin=talk["time"], + duration=datetime.timedelta(minutes=talk["duration"]), + transparent=True, + ) + # ics.py has some problems with unicode + # unidecode replaces letters with their most similar ASCII counterparts + # (ie: accents get stripped) + if "text" in talk: + e.description = _decode(talk["text"]) + e.url = pelican.settings["SCHEDULEURL"] + "#talk-" + talk["id"] + if "room" in talk: + e.location = talk["room"] + + return e + + +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",)) + check_overlaps() + + def generate_output(self, writer=None): + for talkname in sorted(self.talks): + if "resources" in self.talks[talkname]: + 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) + if ICS_ENABLED: + with io.open( + os.path.join(self.output_path, pelican.settings.get("TALKS_ICS")), + "w", + encoding="utf8", + ) as buf: + buf.write(talks_to_ics()) + else: + logging.warning( + "module `ics` not found. " "ICS calendar will not be generated" + ) + + +def add_talks_option_defaults(pelican): + 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) + + +def get_generators(gen): + return TalksGenerator + + +def pelican_init(pelicanobj): + global pelican + pelican = pelicanobj + + +try: + import yaml +except ImportError: + print("ERROR: yaml not found. Talks plugins will be disabled") + + def register(): + pass + + +else: + + def register(): + signals.initialized.connect(pelican_init) + signals.get_generators.connect(get_generators) + signals.initialized.connect(add_talks_option_defaults) + directives.register_directive("talklist", TalkListDirective) + directives.register_directive("talk", TalkDirective) + directives.register_directive("talkgrid", TalkGridDirective)