PonyConf/cfp/planning.py

313 lines
14 KiB
Python
Raw Normal View History

from django.db.models import Q, Prefetch
2017-08-15 11:26:15 +00:00
from django.utils.safestring import mark_safe
from django.utils.html import escape
2017-11-19 20:56:15 +00:00
from django.utils.timezone import localtime, now
2017-08-15 11:26:15 +00:00
from django.core.cache import cache
2017-12-10 12:32:19 +00:00
from django.urls import reverse
2017-09-10 21:27:59 +00:00
from django.template.loader import get_template
2017-09-18 12:01:27 +00:00
from django.conf import settings
2017-08-15 11:26:15 +00:00
from datetime import datetime, timedelta
from copy import deepcopy
from collections import OrderedDict, namedtuple
from itertools import islice
2017-08-28 09:34:27 +00:00
from zlib import adler32
import xml.etree.ElementTree as ET
2017-09-18 12:01:27 +00:00
from icalendar import Calendar as iCalendar, Event as iEvent
2017-08-15 11:26:15 +00:00
from .models import Conference, Talk, Room, Tag
2017-08-15 11:26:15 +00:00
Event = namedtuple('Event', ['talk', 'row', 'rowcount'])
class Program:
def __init__(self, site, pending=False, cache=None, staff=False):
2017-08-15 11:26:15 +00:00
self.site = site
self.pending = pending
2017-11-19 20:56:15 +00:00
if cache is None:
self.cache = not settings.DEBUG
else:
self.cache = cache
self.staff = staff
2017-08-15 11:26:15 +00:00
self.initialized = False
def _lazy_init(self):
self.conference = Conference.objects.get(site=self.site)
self.talks = Talk.objects.\
exclude(category__label__exact='').\
2017-11-22 21:37:27 +00:00
exclude(confirmed=False).\
2017-08-15 11:26:15 +00:00
filter(site=self.site, room__isnull=False, start_date__isnull=False).\
filter(Q(duration__gt=0) | Q(category__duration__gt=0)).\
prefetch_related(
Prefetch('tags', queryset=Tag.objects.filter(staff=True), to_attr='staff_tags'),
Prefetch('tags', queryset=Tag.objects.filter(public=True), to_attr='public_tags'),
'category', 'speakers', 'track', 'tags', 'room',
)
2017-08-15 11:26:15 +00:00
if self.pending:
self.talks = self.talks.exclude(accepted=False)
else:
self.talks = self.talks.filter(accepted=True)
self.talks = self.talks.order_by('start_date')
self.rooms = Room.objects.filter(talk__in=self.talks.all()).order_by('name').distinct()
self.days = {}
for talk in self.talks.all():
duration = talk.estimated_duration
assert(duration)
dt1 = talk.start_date
d1 = localtime(dt1).date()
if d1 not in self.days.keys():
self.days[d1] = {'timeslots': []}
dt2 = dt1 + timedelta(minutes=duration)
d2 = localtime(dt2).date()
if d2 not in self.days.keys():
self.days[d2] = {'timeslots': []}
if dt1 not in self.days[d1]['timeslots']:
self.days[d1]['timeslots'].append(dt1)
if dt2 not in self.days[d2]['timeslots']:
self.days[d2]['timeslots'].append(dt2)
self.cols = OrderedDict([(room, 1) for room in self.rooms])
for day in self.days.keys():
self.days[day]['timeslots'] = sorted(self.days[day]['timeslots'])
self.days[day]['rows'] = OrderedDict([(timeslot, OrderedDict([(room, []) for room in self.rooms])) for timeslot in self.days[day]['timeslots'][:-1]])
for talk in self.talks.exclude(plenary=True).all():
self._add_talk(talk)
for talk in self.talks.filter(plenary=True).all():
self._add_talk(talk)
self.initialized = True
def _add_talk(self, talk):
room = talk.room
dt1 = talk.start_date
d1 = localtime(dt1).date()
dt2 = talk.start_date + timedelta(minutes=talk.estimated_duration)
d2 = localtime(dt2).date()
assert(d1 == d2) # this is a current limitation
dt1 = self.days[d1]['timeslots'].index(dt1)
dt2 = self.days[d1]['timeslots'].index(dt2)
col = None
for row, timeslot in enumerate(islice(self.days[d1]['timeslots'], dt1, dt2)):
if col is None:
col = 0
while col < len(self.days[d1]['rows'][timeslot][room]) and self.days[d1]['rows'][timeslot][room][col]:
col += 1
self.cols[room] = max(self.cols[room], col+1)
event = Event(talk=talk, row=row, rowcount=dt2-dt1)
while len(self.days[d1]['rows'][timeslot][room]) <= col:
self.days[d1]['rows'][timeslot][room].append(None)
self.days[d1]['rows'][timeslot][room][col] = event
def _html_header(self):
output = '<td>Room</td>'
room_cell = '<td%(options)s>%(name)s<br><b>%(label)s</b></td>'
for room, colspan in self.cols.items():
options = ' style="min-width: 100px;" colspan="%d"' % colspan
output += room_cell % {'name': escape(room.name), 'label': escape(room.label), 'options': options}
return '<tr>%s</tr>' % output
def _html_body(self):
output = ''
for day in sorted(self.days.keys()):
output += self._html_day_header(day)
output += self._html_day(day)
return output
def _html_day_header(self, day):
row = '<tr><td colspan="%(colcount)s"><h3>%(day)s</h3></td></tr>'
colcount = 1
for room, col in self.cols.items():
colcount += col
return row % {
'colcount': colcount,
'day': datetime.strftime(day, '%A %d %B'),
}
def _html_day(self, day):
output = []
rows = self.days[day]['rows']
for ts, rooms in rows.items():
output.append(self._html_row(day, ts, rooms))
return '\n'.join(output)
def _html_row(self, day, ts, rooms):
row = '<tr style="%(style)s">%(timeslot)s%(content)s</tr>'
cell = '<td%(options)s>%(content)s</td>'
content = ''
for room, events in rooms.items():
colspan = 1
for i in range(self.cols[room]):
options = ' colspan="%d"' % colspan
cellcontent = ''
if i < len(events) and events[i]:
event = events[i]
if event.row != 0:
continue
options = ' rowspan="%d" bgcolor="%s"' % (event.rowcount, event.talk.category.color)
cellcontent = escape(str(event.talk)) + '<br><em>' + escape(event.talk.get_speakers_str()) + '</em>'
if self.staff:
tags = event.talk.staff_tags
else:
tags = event.talk.public_tags
for tag in tags:
2017-11-16 10:01:27 +00:00
cellcontent += '<br>' + tag.label
2017-08-15 11:26:15 +00:00
elif (i+1 > len(events) or not events[i+1]) and i+1 < self.cols[room]:
colspan += 1
continue
colspan = 1
content += cell % {'options': options, 'content': mark_safe(cellcontent)}
style, timeslot = self._html_timeslot(day, ts)
return row % {
'style': style,
'timeslot': timeslot,
'content': content,
}
def _html_timeslot(self, day, ts):
template = '<td>%(content)s</td>'
start = ts
end = self.days[day]['timeslots'][self.days[day]['timeslots'].index(ts)+1]
duration = (end - start).seconds / 60
date_to_string = lambda date: datetime.strftime(localtime(date), '%H:%M')
style = 'height: %dpx;' % int(duration * 1.2)
timeslot = '<td>%s %s</td>' % tuple(map(date_to_string, [start, end]))
return style, timeslot
def _as_html(self):
template = """<table class="table table-bordered text-center">\n%(header)s\n%(body)s\n</table>"""
if not self.initialized:
self._lazy_init()
return template % {
'header': self._html_header(),
'body': self._html_body(),
}
def _as_xml(self):
if not self.initialized:
self._lazy_init()
schedule = ET.Element('schedule')
2017-08-15 11:26:15 +00:00
conference = ET.SubElement(schedule, 'conference')
elt = ET.SubElement(conference, 'title')
elt.text = self.conference.name
elt = ET.SubElement(conference, 'venue')
elt.text = ', '.join(map(lambda x: x.strip(), self.conference.venue.split('\n')))
elt = ET.SubElement(conference, 'city')
elt.text = self.conference.city
2017-11-27 20:17:20 +00:00
if self.days:
elt = ET.SubElement(conference, 'start_date')
elt.text = sorted(self.days.keys())[0].strftime('%Y-%m-%d')
elt = ET.SubElement(conference, 'end_date')
elt.text = sorted(self.days.keys(), reverse=True)[0].strftime('%Y-%m-%d')
elt = ET.SubElement(conference, 'days_count')
elt.text = str(len(self.days))
2017-08-15 11:26:15 +00:00
for index, day in enumerate(sorted(self.days.keys())):
day_elt = ET.SubElement(schedule, 'day', index=str(index+1), date=day.strftime('%Y-%m-%d'))
2017-08-15 11:26:15 +00:00
for room in self.rooms.all():
room_elt = ET.SubElement(day_elt, 'room', name=room.name)
2017-08-15 11:26:15 +00:00
for talk in self.talks.filter(room=room).order_by('start_date'):
if localtime(talk.start_date).date() != day:
continue
talk_elt = ET.SubElement(day_elt, 'event', id=str(talk.id))
2017-08-15 11:26:15 +00:00
duration = talk.estimated_duration
persons_elt = ET.SubElement(talk_elt, 'persons')
2017-08-15 11:26:15 +00:00
for speaker in talk.speakers.all():
person_elt = ET.SubElement(talk_elt, 'person', id=str(speaker.id))
person_elt.text = str(speaker)
# #if talk.registration_required and self.conference.subscriptions_open:
# # links += mark_safe("""
# # <link tag="registration">%(link)s</link>""" % {
# # 'link': reverse('register-for-a-talk', args=[talk.slug]),
# # })
# # registration = """
# # <attendees_max>%(max)s</attendees_max>
# # <attendees_remain>%(remain)s</attendees_remain>""" % {
# # 'max': talk.attendees_limit,
# # 'remain': talk.remaining_attendees or 0,
# # }
2017-11-06 23:44:57 +00:00
tags_elt = ET.SubElement(talk_elt, 'tags')
for tag in talk.tags.filter(public=True):
tag_elt = ET.SubElement(tags_elt, 'tag', slug=str(tag.slug))
tag_elt.text = tag.name
elt = ET.SubElement(talk_elt, 'start')
elt.text = localtime(talk.start_date).strftime('%H:%M')
elt = ET.SubElement(talk_elt, 'duration')
elt.text = '%02d:%02d' % (talk.estimated_duration / 60, talk.estimated_duration % 60)
elt = ET.SubElement(talk_elt, 'room')
elt.text = room.name
elt = ET.SubElement(talk_elt, 'slug')
elt.text = talk.slug
elt = ET.SubElement(talk_elt, 'title')
elt.text = talk.title
elt = ET.SubElement(talk_elt, 'subtitle')
elt = ET.SubElement(talk_elt, 'track')
2017-10-16 17:10:06 +00:00
elt.text = str(talk.track) if talk.track else ''
elt = ET.SubElement(talk_elt, 'type')
elt.text = talk.category.label
elt = ET.SubElement(talk_elt, 'language')
elt = ET.SubElement(talk_elt, 'description')
elt.text = talk.description
2017-09-27 21:05:54 +00:00
links_elt = ET.SubElement(talk_elt, 'links')
if talk.materials:
elt = ET.SubElement(links_elt, 'link', tag='slides')
elt.text = talk.materials.url
2017-11-27 20:17:20 +00:00
if talk.video and self.conference.videos_available:
2017-09-27 21:05:54 +00:00
elt = ET.SubElement(links_elt, 'link', tag='video')
elt.text = talk.video
2017-08-15 11:26:15 +00:00
return ET.tostring(schedule)
2017-08-15 11:26:15 +00:00
2017-11-19 20:56:15 +00:00
def _as_ics(self, citymeo=False):
2017-08-15 11:26:15 +00:00
if not self.initialized:
self._lazy_init()
2017-09-18 12:01:27 +00:00
cal = iCalendar()
cal.add('prodid', '-//PonyConf.io//PonyConf//FR')
cal.add('version', '2.0')
cal.add('x-wr-calname', self.conference.name)
cal.add('x-wr-timezone', settings.TIME_ZONE)
cal.add('calscale', 'GREGORIAN')
2017-11-19 20:56:15 +00:00
talks = self.talks
if citymeo and talks.exists():
talks = talks.filter(start_date__gte=now()-timedelta(minutes=5))
if talks.exists():
limit = talks.first().start_date.replace(hour=23, minute=59, second=59)
talks = talks.filter(start_date__lte=limit)
for talk in talks:
2017-09-18 12:01:27 +00:00
event = iEvent()
event.add('dtstart', talk.start_date)
if not talk.end_date:
continue
event.add('dtend', talk.end_date)
event.add('dtstamp', talk.updated)
event.add('summary', talk.title)
if talk.room:
event.add('location', talk.room)
event.add('status', 'CONFIRMED' if talk.accepted else 'TENTATIVE')
2017-11-19 20:56:15 +00:00
if not citymeo:
event.add('description', talk.description)
2017-09-18 12:01:27 +00:00
event.add('uid', '%s/%s' % (self.site.domain, talk.id))
cal.add_component(event)
return cal.to_ical()
2017-08-15 11:26:15 +00:00
2017-11-19 20:56:15 +00:00
def render(self, output='html', **kwargs):
2017-08-15 11:26:15 +00:00
if self.cache:
2017-11-19 20:56:15 +00:00
cache_entry = 'ponyconf-%d' % adler32('|'.join(map(str, [self.site.domain, output, self.pending] + list(kwargs.values()))).encode('utf-8'))
2017-08-15 11:26:15 +00:00
result = cache.get(cache_entry)
if not result:
2017-11-19 20:56:15 +00:00
result = getattr(self, '_as_%s' % output)(**kwargs)
2017-08-15 11:26:15 +00:00
cache.set(cache_entry, result, 3 * 60 * 60) # 3H
2017-12-10 12:32:19 +00:00
return result
2017-08-15 11:26:15 +00:00
else:
2017-12-10 12:32:19 +00:00
return getattr(self, '_as_%s' % output)(**kwargs)
2017-08-15 11:26:15 +00:00
def __str__(self):
return self.render()