OpenSlides/openslides/motions/pdf.py

389 lines
15 KiB
Python
Raw Normal View History

import random
import re
from html import escape
from operator import attrgetter
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext as _
from natsort import natsorted
2013-02-03 18:18:29 +01:00
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import PageBreak, Paragraph, Spacer, Table, TableStyle
2013-02-03 18:18:29 +01:00
2015-06-29 12:08:15 +02:00
from openslides.core.config import config
2013-02-03 18:18:29 +01:00
from openslides.utils.pdf import stylesheet
from .models import Category
2013-02-03 18:18:29 +01:00
2013-02-05 18:46:46 +01:00
def motions_to_pdf(pdf, motions):
2013-04-21 19:12:50 +02:00
"""
Create a PDF with all motions.
"""
motions = natsorted(motions, key=attrgetter('identifier'))
2013-02-03 18:18:29 +01:00
all_motion_cover(pdf, motions)
for motion in motions:
pdf.append(PageBreak())
motion_to_pdf(pdf, motion)
2013-02-03 20:12:44 +01:00
2013-02-03 18:18:29 +01:00
def motion_to_pdf(pdf, motion):
2013-04-21 19:12:50 +02:00
"""
Create a PDF for one motion.
"""
identifier = ''
if motion.identifier:
identifier = ' %s' % motion.identifier
pdf.append(Paragraph('%s%s: %s' % (_('Motion'), identifier, escape(motion.title)), stylesheet['Heading1']))
2013-02-03 18:18:29 +01:00
motion_data = []
# submitter
cell1a = []
cell1a.append(Spacer(0, 0.2 * cm))
2013-02-03 20:12:44 +01:00
cell1a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" % _("Submitter"),
stylesheet['Heading4']))
2013-02-03 18:18:29 +01:00
cell1b = []
cell1b.append(Spacer(0, 0.2 * cm))
for submitter in motion.submitters.all():
cell1b.append(Paragraph(str(submitter), stylesheet['Normal']))
2013-02-03 18:18:29 +01:00
motion_data.append([cell1a, cell1b])
# TODO: choose this in workflow
2013-02-19 23:43:20 +01:00
if motion.state.allow_submitter_edit:
2013-02-03 18:18:29 +01:00
# Cell for the signature
cell2a = []
cell2b = []
2013-02-03 20:12:44 +01:00
cell2a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" %
_("Signature"), stylesheet['Heading4']))
2013-02-03 18:18:29 +01:00
cell2b.append(Paragraph(42 * "_", stylesheet['Signaturefield']))
cell2b.append(Spacer(0, 0.1 * cm))
cell2b.append(Spacer(0, 0.2 * cm))
motion_data.append([cell2a, cell2b])
# supporters
if config['motions_min_supporters']:
2013-02-03 18:18:29 +01:00
cell3a = []
cell3b = []
2013-02-03 20:12:44 +01:00
cell3a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font><seqreset id='counter'>"
% _("Supporters"), stylesheet['Heading4']))
supporters = motion.supporters.all()
2013-02-03 18:18:29 +01:00
for supporter in supporters:
cell3b.append(Paragraph("<seq id='counter'/>.&nbsp; %s" % str(supporter),
stylesheet['Normal']))
2013-02-03 18:18:29 +01:00
cell3b.append(Spacer(0, 0.2 * cm))
motion_data.append([cell3a, cell3b])
# Motion state
cell4a = []
cell4b = []
2013-02-03 20:12:44 +01:00
cell4a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" % _("State"),
stylesheet['Heading4']))
cell4b.append(Paragraph(_(motion.state.name), stylesheet['Normal']))
2013-02-03 18:18:29 +01:00
motion_data.append([cell4a, cell4b])
# Version number
2013-02-03 18:18:29 +01:00
if motion.versions.count() > 1:
version = motion.get_active_version()
2013-02-03 18:18:29 +01:00
cell5a = []
cell5b = []
2013-02-03 20:12:44 +01:00
cell5a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" % _("Version"),
stylesheet['Heading4']))
cell5b.append(Paragraph("%s" % version.version_number, stylesheet['Normal']))
2013-02-03 18:18:29 +01:00
motion_data.append([cell5a, cell5b])
# voting result
2013-02-03 18:18:29 +01:00
polls = []
for poll in motion.polls.all():
if not poll.has_votes():
continue
polls.append(poll)
if polls:
cell6a = []
cell6b = []
2013-02-03 20:12:44 +01:00
cell6a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" %
_("Vote result"), stylesheet['Heading4']))
2013-02-03 18:18:29 +01:00
ballotcounter = 0
for poll in polls:
ballotcounter += 1
option = poll.get_options()[0]
yes, no, abstain = (option['Yes'], option['No'], option['Abstain'])
valid, invalid, votescast = ('', '', '')
if poll.votesvalid is not None:
valid = "<br/>%s: %s" % (_("Valid votes"), poll.print_votesvalid())
if poll.votesinvalid is not None:
invalid = "<br/>%s: %s" % (_("Invalid votes"), poll.print_votesinvalid())
if poll.votescast is not None:
votescast = "<br/>%s: %s" % (_("Votes cast"), poll.print_votescast())
2013-02-03 18:18:29 +01:00
if len(polls) > 1:
2013-02-03 20:12:44 +01:00
cell6b.append(Paragraph("%s. %s" % (ballotcounter, _("Vote")),
stylesheet['Bold']))
2013-02-03 18:18:29 +01:00
cell6b.append(Paragraph(
"%s: %s <br/> %s: %s <br/> %s: %s <br/> %s %s %s" %
2015-12-07 12:40:30 +01:00
(_("Yes"), yes, _("No"), no, _("Abstain"), abstain, valid, invalid, votescast),
stylesheet['Normal']))
2013-02-03 18:18:29 +01:00
cell6b.append(Spacer(0, 0.2 * cm))
motion_data.append([cell6a, cell6b])
# Creating Table
table = Table(motion_data)
table._argW[0] = 4.5 * cm
table._argW[1] = 11 * cm
table.setStyle(TableStyle([('BOX', (0, 0), (-1, -1), 1, colors.black),
2013-02-03 20:12:44 +01:00
('VALIGN', (0, 0), (-1, -1), 'TOP')]))
2013-02-03 18:18:29 +01:00
pdf.append(table)
pdf.append(Spacer(0, 1 * cm))
# motion title
pdf.append(Paragraph(escape(motion.title), stylesheet['Heading3']))
# motion text
convert_html_to_reportlab(pdf, motion.text)
pdf.append(Spacer(0, 1 * cm))
# motion reason
2013-02-03 18:18:29 +01:00
if motion.reason:
pdf.append(Paragraph(_("Reason") + ":", stylesheet['Heading3']))
convert_html_to_reportlab(pdf, motion.reason)
2013-02-03 18:18:29 +01:00
return pdf
def convert_html_to_reportlab(pdf, text):
# parsing and replacing not supported html tags for reportlab...
soup = BeautifulSoup(text, "html5lib")
# number ol list elements
ols = soup.find_all('ol')
for ol in ols:
counter = 0
for li in ol.children:
if li.name == 'li':
# if start attribute is available set counter for first list element
if li.parent.get('start') and not li.find_previous_sibling():
counter = int(ol.get('start'))
else:
counter += 1
if li.get('value'):
counter = li.get('value')
else:
li['value'] = counter
# read all list elements...
for element in soup.find_all('li'):
# ... and replace ul list elements with <para><bullet>&bull;</bullet>...<para>
if element.parent.name == "ul":
# nested lists
if element.ul or element.ol:
for i in element.find_all('li'):
element.insert_before(i)
element.clear()
else:
element.name = "para"
bullet_tag = soup.new_tag("bullet")
bullet_tag.string = u""
element.insert(0, bullet_tag)
# ... and replace ol list elements with <para><bullet><seqreset id="%id" base="value"><seq id="%id"></seq>.</bullet>...</para>
if element.parent.name == "ol":
counter = None
# set list id if element is the first of numbered list
if not element.find_previous_sibling():
id = random.randrange(0, 101)
if element.parent.get('start'):
counter = element.parent.get('start')
if element.get('value'):
counter = element.get('value')
# nested lists
if element.ul or element.ol:
nested_list = element.find_all('li')
for i in reversed(nested_list):
element.insert_after(i)
element.attrs = {}
element.name = "para"
element.insert(0, soup.new_tag("bullet"))
element.bullet.insert(0, soup.new_tag("seq"))
element.bullet.seq['id'] = id
if counter:
element.bullet.insert(0, soup.new_tag("seqreset"))
element.bullet.seqreset['id'] = id
element.bullet.seqreset['base'] = int(counter) - 1
element.bullet.insert(2, ".")
# remove tags which are not supported by reportlab (replace tags with their children tags)
for tag in soup.find_all('ul'):
tag.unwrap()
for tag in soup.find_all('ol'):
tag.unwrap()
for tag in soup.find_all('li'):
tag.unwrap()
# use tags which are supported by reportlab
# replace <s> to <strike>
for tag in soup.find_all('s'):
tag.name = "strike"
# replace <del> to <strike>
for tag in soup.find_all('del'):
tag.name = "strike"
for tag in soup.find_all('a'):
# remove a tags without href attribute
if not tag.get('href'):
tag.extract()
for tag in soup.find_all('img'):
# remove img tags without src attribute
if not tag.get('src'):
tag.extract()
# replace style attributes in <span> tags
for tag in soup.find_all('span'):
if tag.get('style'):
# replace style attribute "text-decoration: line-through;" to <strike> tag
if 'text-decoration: line-through' in str(tag['style']):
strike_tag = soup.new_tag("strike")
strike_tag.string = tag.string
tag.replace_with(strike_tag)
# replace style attribute "text-decoration: underline;" to <u> tag
elif 'text-decoration: underline' in str(tag['style']):
u_tag = soup.new_tag("u")
u_tag.string = tag.string
tag.replace_with(u_tag)
# replace style attribute "color: #xxxxxx;" to "<font backcolor='#xxxxxx'>...</font>"
elif 'background-color: ' in str(tag['style']):
font_tag = soup.new_tag("font")
color = re.findall('background-color: (.*?);', str(tag['style']))
if color:
font_tag['backcolor'] = color
if tag.string:
font_tag.string = tag.string
tag.replace_with(font_tag)
# replace style attribute "color: #xxxxxx;" to "<font color='#xxxxxx'>...</font>"
elif 'color: ' in str(tag['style']):
font_tag = soup.new_tag("font")
color = re.findall('color: (.*?);', str(tag['style']))
if color:
font_tag['color'] = color
if tag.string:
font_tag.string = tag.string
tag.replace_with(font_tag)
else:
tag.unwrap()
else:
tag.unwrap()
# print paragraphs with numbers
text = soup.body.contents
paragraph_number = 1
for paragraph in text:
paragraph = str(paragraph)
# ignore empty paragraphs (created by newlines/tabs of ckeditor)
if paragraph == '\n' or paragraph == '\n\n' or paragraph == '\n\t':
continue
if "<pre>" in paragraph:
txt = paragraph.replace('\n', '<br/>').replace(' ', '&nbsp;')
if config["motions_pdf_paragraph_numbering"]:
pdf.append(Paragraph(txt, stylesheet['InnerMonotypeParagraph'], str(paragraph_number)))
paragraph_number += 1
else:
pdf.append(Paragraph(txt, stylesheet['InnerMonotypeParagraph']))
elif "<para>" in paragraph:
pdf.append(Paragraph(paragraph, stylesheet['InnerListParagraph']))
elif "<seqreset" in paragraph:
pass
elif "<h1>" in paragraph:
pdf.append(Paragraph(paragraph, stylesheet['InnerH1Paragraph']))
elif "<h2>" in paragraph:
pdf.append(Paragraph(paragraph, stylesheet['InnerH2Paragraph']))
elif "<h3>" in paragraph:
pdf.append(Paragraph(paragraph, stylesheet['InnerH3Paragraph']))
else:
if config["motions_pdf_paragraph_numbering"]:
pdf.append(Paragraph(paragraph, stylesheet['InnerParagraph'], str(paragraph_number)))
paragraph_number += 1
else:
pdf.append(Paragraph(paragraph, stylesheet['InnerParagraph']))
2013-02-03 18:18:29 +01:00
def all_motion_cover(pdf, motions):
2013-04-21 19:12:50 +02:00
"""
Create a coverpage for all motions.
"""
pdf.append(Paragraph(escape(config["motions_pdf_title"]), stylesheet['Heading1']))
2013-02-03 18:18:29 +01:00
preamble = escape(config["motions_pdf_preamble"])
2013-02-03 18:18:29 +01:00
if preamble:
2013-02-03 20:12:44 +01:00
pdf.append(Paragraph("%s" % preamble.replace('\r\n', '<br/>'), stylesheet['Paragraph']))
2013-02-03 18:18:29 +01:00
pdf.append(Spacer(0, 0.75 * cm))
# list of categories
categories = False
for i, category in enumerate(Category.objects.all()):
categories = True
if i == 0:
pdf.append(Paragraph(_("Categories"), stylesheet['Heading2']))
pdf.append(Paragraph("%s &nbsp;&nbsp; %s" % (escape(category.prefix), escape(category.name)), stylesheet['Paragraph']))
if categories:
pdf.append(PageBreak())
# list of motions
2013-02-03 18:18:29 +01:00
if not motions:
pdf.append(Paragraph(_("No motions available."), stylesheet['Heading3']))
else:
for motion in motions:
identifier = ''
if motion.identifier:
identifier = ' %s' % motion.identifier
pdf.append(Paragraph('%s%s: %s' % (_('Motion'), identifier, escape(motion.title)), stylesheet['Heading3']))
2013-04-21 19:12:50 +02:00
def motion_poll_to_pdf(pdf, poll):
circle = "*" # = Unicode Character 'HEAVY LARGE CIRCLE' (U+2B55)
2013-04-21 19:12:50 +02:00
cell = []
cell.append(Spacer(0, 0.8 * cm))
cell.append(Paragraph(_("Motion No. %s") % poll.motion.identifier, stylesheet['Ballot_title']))
cell.append(Paragraph(poll.motion.title, stylesheet['Ballot_subtitle']))
cell.append(Spacer(0, 0.5 * cm))
cell.append(Paragraph("<font name='circlefont' size='15'>%s</font> <font name='Ubuntu'>%s</font>"
% (circle, _("Yes")), stylesheet['Ballot_option']))
cell.append(Paragraph("<font name='circlefont' size='15'>%s</font> <font name='Ubuntu'>%s</font>"
% (circle, _("No")), stylesheet['Ballot_option']))
cell.append(Paragraph("<font name='circlefont' size='15'>%s</font> <font name='Ubuntu'>%s</font>"
2015-12-07 12:40:30 +01:00
% (circle, _("Abstain")), stylesheet['Ballot_option']))
2013-04-21 19:12:50 +02:00
data = []
# get ballot papers config values
ballot_papers_selection = config["motions_pdf_ballot_papers_selection"]
ballot_papers_number = config["motions_pdf_ballot_papers_number"]
2013-04-21 19:12:50 +02:00
# set number of ballot papers
if ballot_papers_selection == "NUMBER_OF_DELEGATES":
if 'openslides.users' in settings.INSTALLED_APPS:
from openslides.users.models import Group
try:
if Group.objects.get(pk=3):
number = get_user_model().objects.filter(groups__pk=3).count()
except Group.DoesNotExist:
number = 0
else:
number = 0
2013-04-21 19:12:50 +02:00
elif ballot_papers_selection == "NUMBER_OF_ALL_PARTICIPANTS":
number = int(get_user_model().objects.count())
2013-04-21 19:12:50 +02:00
else: # ballot_papers_selection == "CUSTOM_NUMBER"
number = int(ballot_papers_number)
number = max(1, number)
# print ballot papers
if number > 0:
# TODO: try [cell, cell] * (number / 2)
2016-02-23 22:05:15 +01:00
for user in range(int(number / 2)):
2013-04-21 19:12:50 +02:00
data.append([cell, cell])
rest = number % 2
if rest:
data.append([cell, ''])
t = Table(data, 10.5 * cm, 7.42 * cm)
t.setStyle(TableStyle(
[('GRID', (0, 0), (-1, -1), 0.25, colors.grey),
('VALIGN', (0, 0), (-1, -1), 'TOP')]))
pdf.append(t)