Browse Source

Merge branch 'plugin-refactor'

sistemato il plugin talks per avere un tema
boyska 5 years ago
parent
commit
dd0c0a6af1
4 changed files with 666 additions and 521 deletions
  1. 0 521
      plugins/talks.py
  2. 3 0
      plugins/talks/__init__.py
  3. 104 0
      plugins/talks/style.css
  4. 559 0
      plugins/talks/talks.py

+ 0 - 521
plugins/talks.py

@@ -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'<h4>%s</h4>' % 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)

+ 3 - 0
plugins/talks/__init__.py

@@ -0,0 +1,3 @@
+from .talks import *
+
+# flake8: noqa

+ 104 - 0
plugins/talks/style.css

@@ -0,0 +1,104 @@
+
+.talk-resources {
+    word-break: break-all;
+}
+
+.talk-grid {
+    table-layout: auto;
+    min-width: 100%;
+    border-collapse: separate;
+    text-align: center;
+}
+
+.talk-grid > thead th:first-child {
+    max-width: 5em;
+}
+
+.talk-grid > thead th {
+    text-align: center;
+}
+
+.talk-grid tr {
+    height: 1.5em;
+}
+
+
+.rooms-4 .talk {
+    width: 25%;
+}
+
+.rooms-3 .talk {
+    width: 33%;
+}
+
+.rooms-2 .talk {
+    width: 50%;
+}
+
+.rooms-1 .talk {
+    width: 100%;
+}
+
+td.talk {
+    border: 1px solid #444;
+    padding: 4px;
+    vertical-align: middle;
+}
+
+td.talk > a {
+    text-decoration: none;
+    border: none;
+}
+
+.talk-grid tr {
+    line-height: 1em;
+}
+
+.talk-title a,
+.talk-title a:hover,
+.talk-title a:focus {
+    border-bottom: none;
+}
+
+.talk-description strong {
+    background: inherit;
+    color: inherit;
+}
+
+/* tag speciali nei talk {{{ */
+/* generati guardando i tag di glypicon come glyphicon-pushpin e copiandone il campo content */
+td.talk::before {
+  font-family: 'Glyphicons Halflings';
+  float: right;
+}
+td.tag-presentazione_libro::before {
+  content: "\e043";
+}
+td.tag-percorso_base::before {
+  content: "\e146";
+}
+/* tag speciali nei talk }}} */
+
+/* END TALK }}} */
+
+/* Pagine speciali */
+.body-info .entry-content > ul {
+    list-style: none;
+}
+
+/*media query  min dal piccolo, max dal grande*/
+
+@media all and (max-width: 770px) {
+    .talk-grid {
+        font-size: 0.8em;
+    }
+    .talk-grid td {
+        hyphens: auto;
+    }
+}
+@media all and (max-width: 450px) {
+    .talk-grid {
+        font-size: 0.5em;
+    }
+}
+

+ 559 - 0
plugins/talks/talks.py

@@ -0,0 +1,559 @@
+# -*- 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
+import inspect
+
+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
+
+
+@memoize
+def get_css():
+    plugindir = os.path.dirname(
+        os.path.abspath(inspect.getfile(inspect.currentframe()))
+    )
+    with open(os.path.join(plugindir, "style.css")) as buf:
+        return buf.read()
+
+
+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"<h4>%s</h4>" % 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 []
+        css = get_css()
+        if css:
+            output.insert(
+                0,
+                nodes.raw("", '<style type="text/css">%s</style>' % css, format="html"),
+            )
+        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)