#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.shortcuts import render
from django import forms
from django.http import HttpResponse
from django.db.models import Q
from django.utils import timezone
from django.conf import settings
from datetime import timedelta
from collections import defaultdict
import copy
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib import colors
from reportlab.platypus import Table, TableStyle, Paragraph
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.pagesizes import A3, A4, LETTER, landscape
from reportlab.pdfbase.pdfmetrics import registerFont
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.styles import getSampleStyleSheet
from postgresqleu.util.reporttools import cm, mm
from postgresqleu.confreg.models import Room, Track, RegistrationDay, ConferenceSession
from postgresqleu.confreg.util import get_authenticated_conference
def _get_pagesize(size, orient):
so = (size, orient)
if so == ('a4', 'p'):
return A4
if so == ('a4', 'l'):
return landscape(A4)
if so == ('a3', 'p'):
return A3
if so == ('a3', 'l'):
return landscape(A3)
if so == ('letter', 'p'):
return LETTER
if so == ('letter', 'l'):
return landscape(LETTER)
raise Exception("Unknown papersize")
def _setup_canvas(pagesize, orientation):
resp = HttpResponse(content_type='application/pdf')
for font, fontfile in settings.REGISTER_FONTS:
registerFont(TTFont(font, fontfile))
ps = _get_pagesize(pagesize, orientation)
(width, height) = ps
canvas = Canvas(resp, pagesize=ps)
canvas.setAuthor(settings.ORG_NAME)
canvas._doc.info.producer = "{0} Confreg System".format(settings.ORG_SHORTNAME)
return (width, height, canvas, resp)
# Build a linear PDF schedule for a single room only. Can do muptiple days, in which
# case each new day will cause a pagebreak.
def build_linear_pdf_schedule(conference, room, tracks, day, colored, pagesize, orientation, titledatefmt):
q = Q(conference=conference, status=1, starttime__isnull=False, endtime__isnull=False)
q = q & (Q(room=room) | Q(cross_schedule=True))
q = q & (Q(track__in=tracks) | Q(track__isnull=True))
if day:
q = q & Q(starttime__range=(day.day, day.day + timedelta(days=1)))
sessions = ConferenceSession.objects.select_related('track', 'room').filter(q).order_by('starttime')
(width, height, canvas, resp) = _setup_canvas(pagesize, orientation)
# Fetch and modify styles
st_time = getSampleStyleSheet()['Normal']
st_time.fontName = "DejaVu Serif"
st_time.fontSize = 10
st_time.spaceAfter = 8
st_title = getSampleStyleSheet()['Normal']
st_title.fontName = "DejaVu Serif"
st_title.fontSize = 10
st_title.spaceAfter = 8
st_speakers = getSampleStyleSheet()['Normal']
st_speakers.fontName = "DejaVu Serif"
st_speakers.fontSize = 10
st_speakers.spaceAfter = 8
table_horiz_margin = cm(2)
default_tbl_style = [
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('BOX', (0, 0), (-1, -1), 0.5, colors.black),
('INNERGRID', (0, 0), (-1, -1), 0.5, colors.black),
('BOTTOMPADDING', (0, 0), (-1, -1), 15),
]
# Loop over days, creating one page for each day
lastdate = None
tbldata = []
tblstyle = copy.copy(default_tbl_style)
def _finalize_page():
canvas.setFont("DejaVu Serif", 20)
canvas.drawCentredString(width // 2, height - cm(2), "%s - %s" % (room.roomname, lastdate.strftime(titledatefmt)))
t = Table(tbldata, colWidths=[cm(3), width - cm(3) - 2 * table_horiz_margin])
t.setStyle(TableStyle(tblstyle))
w, h = t.wrapOn(canvas, width, height)
t.drawOn(canvas, table_horiz_margin, height - cm(4) - h)
canvas.showPage()
for s in sessions:
if timezone.localdate(s.starttime) != lastdate:
if lastdate is not None:
# New page for a new day!
_finalize_page()
lastdate = timezone.localdate(s.starttime)
tbldata = []
tblstyle = copy.copy(default_tbl_style)
if colored and s.track and s.track.fgcolor:
st_title.textColor = st_speakers.textColor = s.track.fgcolor
else:
st_title.textColor = st_speakers.textColor = colors.black
tstr = Paragraph("%s - %s" % (timezone.localtime(s.starttime).strftime("%H:%M"), timezone.localtime(s.endtime).strftime("%H:%M")), st_time)
if s.cross_schedule:
# Just add a blank row for cross schedule things, so we get the time on there
tbldata.extend([(tstr, '')])
else:
tbldata.extend([(tstr, (Paragraph(s.title, st_title), Paragraph("%s" % s.speaker_list, st_speakers)))])
if colored and s.track and s.track.color:
tblstyle.append(('BACKGROUND', (1, len(tbldata) - 1), (1, len(tbldata) - 1), s.track.color), )
_finalize_page()
canvas.save()
return resp
def build_complete_pdf_schedule(conference, tracks, day, colored, pagesize, orientation, pagesperday, titledatefmt):
pagesperday = int(pagesperday)
q = Q(conference=conference, status=1, starttime__isnull=False, endtime__isnull=False)
q = q & (Q(room__isnull=False) | Q(cross_schedule=True))
q = q & (Q(track__in=tracks) | Q(track__isnull=True))
if day:
q = q & Q(starttime__range=(day.day, day.day + timedelta(days=1)))
sessions = list(ConferenceSession.objects.select_related('track', 'room').filter(q).order_by('starttime', 'room__sortkey', 'room__roomname'))
(width, height, canvas, resp) = _setup_canvas(pagesize, orientation)
groupedbyday = defaultdict(dict)
lastday = None
for s in sessions:
d = timezone.localdate(s.starttime)
if lastday != d:
# New day!
groupedbyday[d] = {
'first': s.starttime,
'last': s.endtime,
'sessions': []
}
lastday = d
groupedbyday[d]['last'] = s.endtime
groupedbyday[d]['sessions'].append(s)
for k, v in list(groupedbyday.items()):
v['length'] = v['last'] - v['first']
v['rooms'] = set([s.room for s in v['sessions'] if s.room])
if not v['rooms']:
return HttpResponse("No rooms used on {}, but cross schedule sessions exist, cannot generate schedule".format(k))
timestampstyle = ParagraphStyle('timestampstyle')
timestampstyle.fontName = "DejaVu Serif"
timestampstyle.fontSize = 8
# Now build one page for each day
for d in sorted(groupedbyday.keys()):
dd = groupedbyday[d]
usableheight = height - 2 * cm(2) - cm(1)
usablewidth = width - 2 * cm(2)
pagesessions = []
currentpagesessions = []
if pagesperday > 1:
# >1 page per day, so we try to find the breakpoints. We do this by locating
# cross-schedule sessions at appropriate times, and including those both on
# the previous and the current schedule.
secondsperpage = dd['length'].seconds // pagesperday
cross_sessions = [s for s in dd['sessions'] if s.cross_schedule]
breakpoints = []
# For each breakpoint, find the closest one
for p in range(1, pagesperday):
breaktime = dd['first'] + timedelta(seconds=p * secondsperpage)
breaksession = cross_sessions[min(list(range(len(cross_sessions))), key=lambda i: abs(cross_sessions[i].starttime - breaktime))]
if breaksession not in breakpoints:
breakpoints.append(breaksession)
for s in dd['sessions']:
currentpagesessions.append(s)
if s in breakpoints:
pagesessions.append(currentpagesessions)
# Make sure the breaking sessions itself is on both pages
currentpagesessions = [s, ]
pagesessions.append(currentpagesessions)
else:
# For a single page schedule, just add all sessions to the first page.
pagesessions.append(dd['sessions'])
# Calculate the vertical size once for all pages, to make sure we get the same size on
# all pages even if the content is different. We do this by picking the *smallest* size
# required for any page (start at usableheight just to be sure it will always get replaced)
unitspersecond = usableheight
for p in pagesessions:
u = usableheight / (p[-1].endtime - p[0].starttime).seconds
if u < unitspersecond:
unitspersecond = u
# Only on the first page in multipage schedules
canvas.setFont("DejaVu Serif", 20)
canvas.drawCentredString(width // 2, height - cm(2), d.strftime(titledatefmt))
roomcount = len(dd['rooms'])
roomwidth = usablewidth // roomcount
# Figure out font size for the room title. Use the biggest one that will still
# fit within the boxes.
roomtitlefontsize = 20
for r in dd['rooms']:
for fs in 16, 14, 12, 10, 8:
fwidth = canvas.stringWidth(r.roomname, "DejaVu Serif", fs)
if fwidth < roomwidth - mm(4):
# Width at this size is small enough to work, so use it
if fs < roomtitlefontsize:
roomtitlefontsize = fs
break
canvas.setFont("DejaVu Serif", roomtitlefontsize)
roompos = {}
for r in sorted(dd['rooms'], key=lambda x: (x.sortkey, x.roomname)):
canvas.rect(cm(2) + len(roompos) * roomwidth, height - cm(4), roomwidth, cm(1), stroke=1)
canvas.drawCentredString(cm(2) + len(roompos) * roomwidth + roomwidth // 2,
height - cm(4) + (cm(1) - roomtitlefontsize) // 2,
r.roomname)
roompos[r] = len(roompos)
for ps in pagesessions:
pagelength = (ps[-1].endtime - ps[0].starttime).seconds
first = ps[0].starttime
canvas.rect(cm(2), height - pagelength * unitspersecond - cm(4), roomcount * roomwidth, pagelength * unitspersecond, stroke=1)
for s in ps:
if s.cross_schedule:
# Cross schedule rooms are very special...
s_left = cm(2)
thisroomwidth = roomcount * roomwidth
else:
s_left = cm(2) + roompos[s.room] * roomwidth
thisroomwidth = roomwidth
s_height = (s.endtime - s.starttime).seconds * unitspersecond
s_top = height - (s.starttime - first).seconds * unitspersecond - s_height - cm(4)
if colored:
if s.track and s.track.color:
canvas.setFillColor(s.track.color)
else:
canvas.setFillColor(colors.white)
canvas.rect(s_left, s_top, thisroomwidth, s_height, stroke=1, fill=colored)
timestampstr = "%s-%s" % (timezone.localtime(s.starttime).strftime("%H:%M"), timezone.localtime(s.endtime).strftime("%H:%M"))
if colored and s.track and s.track.fgcolor:
timestampstyle.textColor = s.track.fgcolor
ts = Paragraph(timestampstr, timestampstyle)
(tsaw, tsah) = ts.wrap(thisroomwidth - mm(2), timestampstyle.fontSize)
ts.drawOn(canvas, s_left + mm(1), s_top + s_height - tsah - mm(1))
if s_height - tsah * 1.2 - mm(2) < tsah:
# This can never fit, since it's smaller than our font size!
# Instead, print as much as possible on the same row as the time
tswidth = canvas.stringWidth(timestampstr, "DejaVu Serif", 8)
title = s.title
trunc = ''
while title:
t = title + trunc
fwidth = canvas.stringWidth(t, "DejaVu Serif", 8)
if fwidth < thisroomwidth - tswidth - mm(2):
# Fits now!
canvas.setFont("DejaVu Serif", 8)
p = Paragraph(t, timestampstyle)
(paw, pah) = p.wrap(thisroomwidth - tswidth - mm(2), timestampstyle.fontSize)
p.drawOn(canvas, s_left + mm(1) + tswidth + mm(1), s_top + s_height - tsah - mm(1))
break
else:
title = title.rpartition(' ')[0]
trunc = '..'
continue
try:
for includespeaker in (True, False):
title = s.title
while title:
for fs in (12, 10, 9, 8):
sessionstyle = ParagraphStyle('sessionstyle')
sessionstyle.fontName = "DejaVu Serif"
sessionstyle.fontSize = fs
if colored and s.track and s.track.fgcolor:
sessionstyle.textColor = s.track.fgcolor
speakersize = fs > 8 and 8 or fs - 1
if includespeaker:
p = Paragraph(title + "
%s" % (speakersize, s.speaker_list), sessionstyle)
else:
p = Paragraph(title, sessionstyle)
(aw, ah) = p.wrap(thisroomwidth - mm(2), s_height - tsah * 1.2 - mm(2))
if ah <= s_height - tsah * 1.2 - mm(2):
# FIT!
p.drawOn(canvas, s_left + mm(1), s_top + s_height - ah - tsah * 1.2 - mm(1))
raise StopIteration
# Too big, so try to chop down the title and run again
# (this is assuming our titles are reasonable length, or we could be
# looping for a *very* long time)
title = "%s.." % title.rpartition(' ')[0]
if title == '..':
title = ''
except StopIteration:
pass
canvas.showPage()
canvas.save()
return resp
class PdfScheduleForm(forms.Form):
room = forms.ModelChoiceField(label='Rooms to include', queryset=None, empty_label='(all rooms)', required=False,
help_text="Selecting all rooms will print a full schedule with each session sized to it's length. Selecting a single room will print that rooms schedule in adaptive sized rows in a table.")
day = forms.ModelChoiceField(label='Days to include', queryset=None, empty_label='(all days)', required=False)
tracks = forms.ModelMultipleChoiceField(label='Tracks to include', queryset=None, required=True, help_text="Filter for some tracks. By default, all tracks are included.")
colored = forms.BooleanField(label='Colored tracks', required=False)
pagesize = forms.ChoiceField(label='Page size', choices=(('a4', 'A4'), ('a3', 'A3'), ('letter', 'Letter')))
orientation = forms.ChoiceField(label='Orientation', choices=(('p', 'Portrait'), ('l', 'Landscape')))
pagesperday = forms.ChoiceField(label='Pages per day', choices=((1, 1), (2, 2), (3, 3)), help_text="Not used for per-room schedules. Page breaks happen only at cross - schedule sessions.")
titledatefmt = forms.CharField(label='Title date format', help_text="strftime format specification used to print the date in the title of the first page for each day")
def __init__(self, conference, *args, **kwargs):
self.conference = conference
alltracks = Track.objects.filter(conference=conference).order_by('sortkey', 'trackname')
kwargs['initial'] = {'titledatefmt': '%A, %b %d', 'tracks': alltracks}
super(PdfScheduleForm, self).__init__(*args, **kwargs)
self.fields['room'].queryset = Room.objects.filter(conference=conference)
self.fields['day'].queryset = RegistrationDay.objects.filter(conference=conference)
self.fields['tracks'].queryset = alltracks
def pdfschedule(request, confname):
conference = get_authenticated_conference(request, confname)
if request.method == "POST":
form = PdfScheduleForm(conference, data=request.POST)
if form.is_valid():
if form.cleaned_data.get('room', None):
return build_linear_pdf_schedule(conference,
form.cleaned_data['room'],
form.cleaned_data['tracks'],
form.cleaned_data.get('day', None),
form.cleaned_data.get('colored', None),
form.cleaned_data.get('pagesize', None),
form.cleaned_data.get('orientation', None),
form.cleaned_data.get('titledatefmt', None),
)
else:
return build_complete_pdf_schedule(conference,
form.cleaned_data['tracks'],
form.cleaned_data.get('day', None),
form.cleaned_data.get('colored', None),
form.cleaned_data.get('pagesize', None),
form.cleaned_data.get('orientation', None),
form.cleaned_data.get('pagesperday', None),
form.cleaned_data.get('titledatefmt', None),
)
# Fall through and render the form again if it's not valid
else:
form = PdfScheduleForm(conference)
return render(request, 'confreg/pdfschedule.html', {
'conference': conference,
'form': form,
'helplink': 'schedule#pdf',
})