Merge pull request #2476 from ostcar/remove_serverside_pdf
Removed old code needed be the server to serve pdf
This commit is contained in:
commit
db38e5e075
18
README.rst
18
README.rst
@ -28,13 +28,7 @@ a. Check requirements
|
|||||||
'''''''''''''''''''''
|
'''''''''''''''''''''
|
||||||
|
|
||||||
Make sure that you have installed `Python (>= 3.4)
|
Make sure that you have installed `Python (>= 3.4)
|
||||||
<https://www.python.org/>`_ on your system. You also need the Python
|
<https://www.python.org/>`_ on your system.
|
||||||
development headers, the Independent JPEG Group's JPEG runtime library
|
|
||||||
(dependency package) and the compression library (development).
|
|
||||||
|
|
||||||
\E. g. for Ubuntu run::
|
|
||||||
|
|
||||||
$ sudo apt-get install python3-dev libjpeg-dev zlib1g-dev
|
|
||||||
|
|
||||||
|
|
||||||
b. Setup a virtual Python environment (optional)
|
b. Setup a virtual Python environment (optional)
|
||||||
@ -156,28 +150,18 @@ OpenSlides uses the following projects or parts of them:
|
|||||||
* `backports-abc <https://github.com/cython/backports_abc>`_,
|
* `backports-abc <https://github.com/cython/backports_abc>`_,
|
||||||
License: Python Software Foundation License
|
License: Python Software Foundation License
|
||||||
|
|
||||||
* `Beautiful Soup <http://www.crummy.com/software/BeautifulSoup/>`_,
|
|
||||||
License: MIT
|
|
||||||
|
|
||||||
* `Django <https://www.djangoproject.com>`_, License: BSD
|
* `Django <https://www.djangoproject.com>`_, License: BSD
|
||||||
|
|
||||||
* `Django REST framework <http://www.django-rest-framework.org>`_, License:
|
* `Django REST framework <http://www.django-rest-framework.org>`_, License:
|
||||||
BSD
|
BSD
|
||||||
|
|
||||||
* `html5lib <https://github.com/html5lib/html5lib-python>`_, License: MIT
|
|
||||||
|
|
||||||
* `Django Channels <https://github.com/andrewgodwin/channels/>`_, License: MIT
|
* `Django Channels <https://github.com/andrewgodwin/channels/>`_, License: MIT
|
||||||
|
|
||||||
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_,
|
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_,
|
||||||
License: MIT
|
License: MIT
|
||||||
|
|
||||||
* `natsort <https://pypi.python.org/pypi/natsort>`_, License: MIT
|
|
||||||
|
|
||||||
* `PyPDF2 <http://mstamy2.github.io/PyPDF2/>`_, License: BSD
|
* `PyPDF2 <http://mstamy2.github.io/PyPDF2/>`_, License: BSD
|
||||||
|
|
||||||
* `ReportLab <http://www.reportlab.com/opensource/>`_,
|
|
||||||
License: BSD
|
|
||||||
|
|
||||||
* `roman <https://pypi.python.org/pypi/roman>`_, License: Python 2.1.1
|
* `roman <https://pypi.python.org/pypi/roman>`_, License: Python 2.1.1
|
||||||
|
|
||||||
* `setuptools <https://pypi.python.org/pypi/setuptools>`_,
|
* `setuptools <https://pypi.python.org/pypi/setuptools>`_,
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
url(r'^print/$',
|
|
||||||
views.AgendaPDF.as_view(),
|
|
||||||
name='agenda_pdf'),
|
|
||||||
]
|
|
@ -1,14 +1,9 @@
|
|||||||
from html import escape
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy
|
|
||||||
from reportlab.platypus import Paragraph
|
|
||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.pdf import stylesheet
|
|
||||||
from openslides.utils.rest_api import (
|
from openslides.utils.rest_api import (
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ListModelMixin,
|
ListModelMixin,
|
||||||
@ -19,7 +14,6 @@ from openslides.utils.rest_api import (
|
|||||||
detail_route,
|
detail_route,
|
||||||
list_route,
|
list_route,
|
||||||
)
|
)
|
||||||
from openslides.utils.views import PDFView
|
|
||||||
|
|
||||||
from .access_permissions import ItemAccessPermissions
|
from .access_permissions import ItemAccessPermissions
|
||||||
from .models import Item, Speaker
|
from .models import Item, Speaker
|
||||||
@ -231,38 +225,3 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
|||||||
"""
|
"""
|
||||||
Item.objects.number_all(numeral_system=config['agenda_numeral_system'])
|
Item.objects.number_all(numeral_system=config['agenda_numeral_system'])
|
||||||
return Response({'detail': _('The agenda has been numbered.')})
|
return Response({'detail': _('The agenda has been numbered.')})
|
||||||
|
|
||||||
|
|
||||||
# Views to generate PDFs
|
|
||||||
|
|
||||||
class AgendaPDF(PDFView):
|
|
||||||
"""
|
|
||||||
Create a full agenda-PDF.
|
|
||||||
"""
|
|
||||||
required_permission = 'agenda.can_see'
|
|
||||||
filename = ugettext_lazy('Agenda')
|
|
||||||
document_title = ugettext_lazy('Agenda')
|
|
||||||
|
|
||||||
def append_to_pdf(self, story):
|
|
||||||
tree = Item.objects.get_tree(only_agenda_items=True, include_content=True)
|
|
||||||
|
|
||||||
def walk_tree(tree, ancestors=0):
|
|
||||||
"""
|
|
||||||
Generator that yields a two-element-tuple. The first element is an
|
|
||||||
agenda-item and the second a number for steps to the root element.
|
|
||||||
"""
|
|
||||||
for element in tree:
|
|
||||||
yield element['item'], ancestors
|
|
||||||
yield from walk_tree(element['children'], ancestors + 1)
|
|
||||||
|
|
||||||
for item, ancestors in walk_tree(tree):
|
|
||||||
item_number = "{} ".format(item.item_number) if item.item_number else ''
|
|
||||||
if ancestors:
|
|
||||||
space = " " * 6 * ancestors
|
|
||||||
story.append(Paragraph(
|
|
||||||
"%s%s%s" % (space, item_number, escape(item.title)),
|
|
||||||
stylesheet['Subitem']))
|
|
||||||
else:
|
|
||||||
story.append(Paragraph(
|
|
||||||
"%s%s" % (item_number, escape(item.title)),
|
|
||||||
stylesheet['Item']))
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
url(r'^print/$',
|
|
||||||
views.AssignmentPDF.as_view(),
|
|
||||||
name='assignments_pdf'),
|
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/print/$',
|
|
||||||
views.AssignmentPDF.as_view(),
|
|
||||||
name='assignments_single_pdf'),
|
|
||||||
|
|
||||||
url(r'^poll/(?P<poll_pk>\d+)/print/$',
|
|
||||||
views.AssignmentPollPDF.as_view(),
|
|
||||||
name='assignmentpoll_pdf'),
|
|
||||||
]
|
|
@ -1,24 +1,7 @@
|
|||||||
from html import escape
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ungettext
|
|
||||||
from reportlab.lib import colors
|
|
||||||
from reportlab.lib.units import cm
|
|
||||||
from reportlab.platypus import (
|
|
||||||
LongTable,
|
|
||||||
PageBreak,
|
|
||||||
Paragraph,
|
|
||||||
SimpleDocTemplate,
|
|
||||||
Spacer,
|
|
||||||
Table,
|
|
||||||
TableStyle,
|
|
||||||
)
|
|
||||||
|
|
||||||
from openslides.core.config import config
|
|
||||||
from openslides.utils.pdf import stylesheet
|
|
||||||
from openslides.utils.rest_api import (
|
from openslides.utils.rest_api import (
|
||||||
DestroyModelMixin,
|
DestroyModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
@ -28,7 +11,6 @@ from openslides.utils.rest_api import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
from openslides.utils.views import PDFView
|
|
||||||
|
|
||||||
from .access_permissions import AssignmentAccessPermissions
|
from .access_permissions import AssignmentAccessPermissions
|
||||||
from .models import Assignment, AssignmentPoll
|
from .models import Assignment, AssignmentPoll
|
||||||
@ -220,365 +202,3 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
|
|||||||
"""
|
"""
|
||||||
return (self.request.user.has_perm('assignments.can_see') and
|
return (self.request.user.has_perm('assignments.can_see') and
|
||||||
self.request.user.has_perm('assignments.can_manage'))
|
self.request.user.has_perm('assignments.can_manage'))
|
||||||
|
|
||||||
|
|
||||||
# Views to generate PDFs
|
|
||||||
|
|
||||||
class AssignmentPDF(PDFView):
|
|
||||||
required_permission = 'assignments.can_see'
|
|
||||||
top_space = 0
|
|
||||||
|
|
||||||
def get_filename(self):
|
|
||||||
try:
|
|
||||||
assignment = Assignment.objects.get(pk=self.kwargs['pk'])
|
|
||||||
filename = u'%s-%s' % (
|
|
||||||
_("Election"),
|
|
||||||
assignment.title.replace(' ', '_'))
|
|
||||||
except:
|
|
||||||
filename = _("Elections")
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def append_to_pdf(self, story):
|
|
||||||
try:
|
|
||||||
assignment_pk = self.kwargs['pk']
|
|
||||||
except KeyError:
|
|
||||||
assignment_pk = None
|
|
||||||
|
|
||||||
if assignment_pk is None: # print all assignments
|
|
||||||
title = escape(config["assignments_pdf_title"])
|
|
||||||
story.append(Paragraph(title, stylesheet['Heading1']))
|
|
||||||
preamble = escape(config["assignments_pdf_preamble"])
|
|
||||||
if preamble:
|
|
||||||
story.append(Paragraph(
|
|
||||||
"%s" % preamble.replace('\r\n', '<br/>'),
|
|
||||||
stylesheet['Paragraph']))
|
|
||||||
story.append(Spacer(0, 0.75 * cm))
|
|
||||||
assignments = Assignment.objects.all()
|
|
||||||
if not assignments: # No assignments existing
|
|
||||||
story.append(Paragraph(
|
|
||||||
_("No elections available."), stylesheet['Heading3']))
|
|
||||||
else: # Print all assignments
|
|
||||||
# List of assignments
|
|
||||||
for assignment in assignments:
|
|
||||||
story.append(Paragraph(
|
|
||||||
escape(assignment.title), stylesheet['Heading3']))
|
|
||||||
# Assignment details (each assignment on single page)
|
|
||||||
for assignment in assignments:
|
|
||||||
story.append(PageBreak())
|
|
||||||
# append the assignment to the story-object
|
|
||||||
self.get_assignment(assignment, story)
|
|
||||||
else: # print selected assignment
|
|
||||||
assignment = Assignment.objects.get(pk=assignment_pk)
|
|
||||||
# append the assignment to the story-object
|
|
||||||
self.get_assignment(assignment, story)
|
|
||||||
|
|
||||||
def get_assignment(self, assignment, story):
|
|
||||||
# title
|
|
||||||
story.append(Paragraph(
|
|
||||||
_("Election: %s") % escape(assignment.title), stylesheet['Heading1']))
|
|
||||||
story.append(Spacer(0, 0.5 * cm))
|
|
||||||
|
|
||||||
# Filling table rows...
|
|
||||||
data = []
|
|
||||||
polls = assignment.polls.filter(published=True)
|
|
||||||
# 1. posts
|
|
||||||
data.append([
|
|
||||||
Paragraph("%s:" %
|
|
||||||
_("Number of members to be elected"), stylesheet['Bold']),
|
|
||||||
Paragraph(str(assignment.open_posts), stylesheet['Paragraph'])])
|
|
||||||
|
|
||||||
# 2a. if no polls available print candidates
|
|
||||||
if not polls:
|
|
||||||
data.append([
|
|
||||||
Paragraph("%s:<seqreset id='counter'>" %
|
|
||||||
_("Candidates"), stylesheet['Heading4']),
|
|
||||||
[]])
|
|
||||||
for candidate in assignment.candidates:
|
|
||||||
data.append([
|
|
||||||
[],
|
|
||||||
Paragraph("<seq id='counter'/>. %s" % candidate,
|
|
||||||
stylesheet['Signaturefield'])])
|
|
||||||
if assignment.phase == assignment.PHASE_SEARCH:
|
|
||||||
for x in range(0, 7):
|
|
||||||
data.append([
|
|
||||||
[],
|
|
||||||
Paragraph("<seq id='counter'/>. "
|
|
||||||
"__________________________________________",
|
|
||||||
stylesheet['Signaturefield'])])
|
|
||||||
|
|
||||||
# 2b. if polls available print election result
|
|
||||||
if polls:
|
|
||||||
# Preparing
|
|
||||||
vote_results = assignment.vote_results(only_published=True)
|
|
||||||
data_votes = []
|
|
||||||
|
|
||||||
# Left side
|
|
||||||
cell = []
|
|
||||||
cell.append(Paragraph(
|
|
||||||
"%s:" % (_("Election result")), stylesheet['Heading4']))
|
|
||||||
|
|
||||||
# Add table head row
|
|
||||||
headrow = []
|
|
||||||
headrow.append(_("Candidates"))
|
|
||||||
for poll in polls:
|
|
||||||
headrow.append("%s. %s" % (poll.get_ballot(), _("ballot")))
|
|
||||||
data_votes.append(headrow)
|
|
||||||
|
|
||||||
# Add result rows
|
|
||||||
elected_candidates = list(assignment.elected)
|
|
||||||
length = len(vote_results)
|
|
||||||
for candidate, poll_list in vote_results.items():
|
|
||||||
row = []
|
|
||||||
candidate_string = candidate.get_short_name()
|
|
||||||
if candidate in elected_candidates:
|
|
||||||
candidate_string = "* " + candidate_string
|
|
||||||
if candidate.structure_level and length < 20:
|
|
||||||
candidate_string += "\n(%s)" % candidate.structure_level
|
|
||||||
row.append(candidate_string)
|
|
||||||
for vote in poll_list:
|
|
||||||
if vote is None:
|
|
||||||
row.append('–')
|
|
||||||
elif 'Yes' in vote and 'No' in vote and 'Abstain' in vote:
|
|
||||||
row.append(
|
|
||||||
_("Y: %(YES)s\nN: %(NO)s\nA: %(ABSTAIN)s")
|
|
||||||
% {'YES': vote['Yes'], 'NO': vote['No'],
|
|
||||||
'ABSTAIN': vote['Abstain']})
|
|
||||||
elif 'Yes' in vote and 'No' in vote:
|
|
||||||
row.append(
|
|
||||||
_("Y: %(YES)s\nN: %(NO)s")
|
|
||||||
% {'YES': vote['Yes'], 'NO': vote['No']})
|
|
||||||
elif 'Votes' in vote:
|
|
||||||
row.append(vote['Votes'])
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
data_votes.append(row)
|
|
||||||
|
|
||||||
# Add valid votes row
|
|
||||||
footrow_one = []
|
|
||||||
footrow_one.append(_("Valid votes"))
|
|
||||||
votesvalid_is_used = False
|
|
||||||
for poll in polls:
|
|
||||||
footrow_one.append(poll.votesvalid)
|
|
||||||
if poll.votesvalid is not None:
|
|
||||||
votesvalid_is_used = True
|
|
||||||
if votesvalid_is_used:
|
|
||||||
data_votes.append(footrow_one)
|
|
||||||
|
|
||||||
# Add invalid votes row
|
|
||||||
footrow_two = []
|
|
||||||
footrow_two.append(_("Invalid votes"))
|
|
||||||
votesinvalid_is_used = False
|
|
||||||
for poll in polls:
|
|
||||||
footrow_two.append(poll.votesinvalid)
|
|
||||||
if poll.votesinvalid is not None:
|
|
||||||
votesinvalid_is_used = True
|
|
||||||
if votesinvalid_is_used:
|
|
||||||
data_votes.append(footrow_two)
|
|
||||||
|
|
||||||
# Add votes cast row
|
|
||||||
footrow_three = []
|
|
||||||
footrow_three.append(_("Votes cast"))
|
|
||||||
votescast_is_used = False
|
|
||||||
for poll in polls:
|
|
||||||
footrow_three.append(poll.votescast)
|
|
||||||
if poll.votescast is not None:
|
|
||||||
votescast_is_used = True
|
|
||||||
if votescast_is_used:
|
|
||||||
data_votes.append(footrow_three)
|
|
||||||
|
|
||||||
table_votes = Table(data_votes)
|
|
||||||
table_votes.setStyle(
|
|
||||||
TableStyle([
|
|
||||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
||||||
('LINEABOVE', (0, 0), (-1, 0), 2, colors.black),
|
|
||||||
('LINEABOVE', (0, 1), (-1, 1), 1, colors.black),
|
|
||||||
('LINEBELOW', (0, -1), (-1, -1), 2, colors.black),
|
|
||||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), (colors.white, (.9, .9, .9)))
|
|
||||||
])
|
|
||||||
)
|
|
||||||
data.append([cell, table_votes])
|
|
||||||
if elected_candidates:
|
|
||||||
data.append(['', '* = ' + _('elected')])
|
|
||||||
|
|
||||||
# table style
|
|
||||||
data.append(['', ''])
|
|
||||||
t = LongTable(data)
|
|
||||||
t._argW[0] = 4.5 * cm
|
|
||||||
t._argW[1] = 11 * cm
|
|
||||||
t.setStyle(TableStyle([
|
|
||||||
('BOX', (0, 0), (-1, -1), 1, colors.black),
|
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP')]))
|
|
||||||
story.append(t)
|
|
||||||
story.append(Spacer(0, 1 * cm))
|
|
||||||
|
|
||||||
# election description
|
|
||||||
story.append(
|
|
||||||
Paragraph("%s" % escape(assignment.description).replace('\r\n', '<br/>'),
|
|
||||||
stylesheet['Paragraph']))
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentPollPDF(PDFView):
|
|
||||||
required_permission = 'assignments.can_manage'
|
|
||||||
top_space = 0
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
self.poll = AssignmentPoll.objects.get(pk=self.kwargs['poll_pk'])
|
|
||||||
return super().get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_filename(self):
|
|
||||||
filename = u'%s-%s_%s' % (
|
|
||||||
_("Election"), self.poll.assignment.title.replace(' ', '_'),
|
|
||||||
self.poll.get_ballot())
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def get_template(self, buffer):
|
|
||||||
return SimpleDocTemplate(
|
|
||||||
buffer, topMargin=-6, bottomMargin=-6, leftMargin=0, rightMargin=0,
|
|
||||||
showBoundary=False)
|
|
||||||
|
|
||||||
def build_document(self, pdf_document, story):
|
|
||||||
pdf_document.build(story)
|
|
||||||
|
|
||||||
def append_to_pdf(self, story):
|
|
||||||
circle = "*" # = Unicode Character 'HEAVY LARGE CIRCLE' (U+2B55)
|
|
||||||
cell = []
|
|
||||||
cell.append(Spacer(0, 0.8 * cm))
|
|
||||||
cell.append(Paragraph(
|
|
||||||
_("Election") + ": " + self.poll.assignment.title,
|
|
||||||
stylesheet['Ballot_title']))
|
|
||||||
cell.append(Paragraph(
|
|
||||||
self.poll.description or '',
|
|
||||||
stylesheet['Ballot_subtitle']))
|
|
||||||
options = self.poll.get_options()
|
|
||||||
|
|
||||||
ballot_string = _("%d. ballot") % self.poll.get_ballot()
|
|
||||||
candidate_string = ungettext(
|
|
||||||
"%d candidate", "%d candidates", len(options)) % len(options)
|
|
||||||
available_posts_string = ungettext(
|
|
||||||
"%d available post", "%d available posts",
|
|
||||||
self.poll.assignment.open_posts) % self.poll.assignment.open_posts
|
|
||||||
cell.append(Paragraph(
|
|
||||||
"%s, %s, %s" % (ballot_string, candidate_string, available_posts_string),
|
|
||||||
stylesheet['Ballot_description']))
|
|
||||||
cell.append(Spacer(0, 0.4 * cm))
|
|
||||||
|
|
||||||
data = []
|
|
||||||
# get ballot papers config values
|
|
||||||
ballot_papers_selection = config["assignments_pdf_ballot_papers_selection"]
|
|
||||||
ballot_papers_number = config["assignments_pdf_ballot_papers_number"]
|
|
||||||
|
|
||||||
# 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
|
|
||||||
elif ballot_papers_selection == "NUMBER_OF_ALL_PARTICIPANTS":
|
|
||||||
number = int(get_user_model().objects.count())
|
|
||||||
else: # ballot_papers_selection == "CUSTOM_NUMBER"
|
|
||||||
number = int(ballot_papers_number)
|
|
||||||
number = max(1, number)
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
cellcolumnA = []
|
|
||||||
# Choose kind of ballot paper (YesNoAbstain, YesNo or Yes)
|
|
||||||
if self.poll.pollmethod in ['yna', 'yn']: # YesNoAbstain/YesNo ballot: max 27 candidates
|
|
||||||
for option in options:
|
|
||||||
counter += 1
|
|
||||||
candidate = option.candidate
|
|
||||||
cell.append(Paragraph(
|
|
||||||
candidate.get_short_name(), stylesheet['Ballot_option_name_YNA']))
|
|
||||||
if candidate.structure_level:
|
|
||||||
cell.append(Paragraph(
|
|
||||||
"(%s)" % candidate.structure_level,
|
|
||||||
stylesheet['Ballot_option_suffix_YNA']))
|
|
||||||
if self.poll.pollmethod == 'yna':
|
|
||||||
cell.append(Paragraph(
|
|
||||||
" ", stylesheet['Ballot_option_suffix_YNA']))
|
|
||||||
cell.append(Paragraph("<font name='circlefont' size='15'>%(circle)s</font> \
|
|
||||||
<font name='Ubuntu'>%(yes)s </font> \
|
|
||||||
<font name='circlefont' size='15'>%(circle)s</font> \
|
|
||||||
<font name='Ubuntu'>%(no)s </font> \
|
|
||||||
<font name='circlefont' size='15'>%(circle)s</font> \
|
|
||||||
<font name='Ubuntu'>%(abstain)s</font>" %
|
|
||||||
{'circle': circle,
|
|
||||||
'yes': _("Yes"),
|
|
||||||
'no': _("No"),
|
|
||||||
'abstain': _("Abstain")},
|
|
||||||
stylesheet['Ballot_option_circle_YNA']))
|
|
||||||
else:
|
|
||||||
cell.append(Paragraph(
|
|
||||||
" ", stylesheet['Ballot_option_suffix_YNA']))
|
|
||||||
cell.append(Paragraph("<font name='circlefont' size='15'>%(circle)s</font> \
|
|
||||||
<font name='Ubuntu'>%(yes)s </font> \
|
|
||||||
<font name='circlefont' size='15'>%(circle)s</font> \
|
|
||||||
<font name='Ubuntu'>%(no)s </font>" %
|
|
||||||
{'circle': circle,
|
|
||||||
'yes': _("Yes"),
|
|
||||||
'no': _("No")},
|
|
||||||
stylesheet['Ballot_option_circle_YNA']))
|
|
||||||
if counter == 13:
|
|
||||||
cellcolumnA = cell
|
|
||||||
cell = []
|
|
||||||
cell.append(Spacer(0, 1.3 * cm))
|
|
||||||
|
|
||||||
# print ballot papers
|
|
||||||
for user in range(number // 2):
|
|
||||||
if len(options) > 13:
|
|
||||||
data.append([cellcolumnA, cell])
|
|
||||||
else:
|
|
||||||
data.append([cell, cell])
|
|
||||||
rest = number % 2
|
|
||||||
if rest:
|
|
||||||
data.append([cell, ''])
|
|
||||||
if len(options) <= 2:
|
|
||||||
t = Table(data, 10.5 * cm, 7.42 * cm)
|
|
||||||
elif len(options) <= 5:
|
|
||||||
t = Table(data, 10.5 * cm, 14.84 * cm)
|
|
||||||
else:
|
|
||||||
t = Table(data, 10.5 * cm, 29.7 * cm)
|
|
||||||
else: # Yes ballot: max 46 candidates
|
|
||||||
for option in options:
|
|
||||||
counter += 1
|
|
||||||
candidate = option.candidate
|
|
||||||
cell.append(Paragraph("<font name='circlefont' size='15'>%s</font> \
|
|
||||||
<font name='Ubuntu'>%s</font>" %
|
|
||||||
(circle, candidate.get_short_name()), stylesheet['Ballot_option_name']))
|
|
||||||
if candidate.structure_level:
|
|
||||||
cell.append(Paragraph(
|
|
||||||
"(%s)" % candidate.structure_level,
|
|
||||||
stylesheet['Ballot_option_suffix']))
|
|
||||||
else:
|
|
||||||
cell.append(Paragraph(
|
|
||||||
" ", stylesheet['Ballot_option_suffix']))
|
|
||||||
if counter == 22:
|
|
||||||
cellcolumnA = cell
|
|
||||||
cell = []
|
|
||||||
cell.append(Spacer(0, 0.75 * cm))
|
|
||||||
|
|
||||||
# print ballot papers
|
|
||||||
for user in range(number // 2):
|
|
||||||
if len(options) > 22:
|
|
||||||
data.append([cellcolumnA, cell])
|
|
||||||
else:
|
|
||||||
data.append([cell, cell])
|
|
||||||
rest = number % 2
|
|
||||||
if rest:
|
|
||||||
data.append([cell, ''])
|
|
||||||
if len(options) <= 4:
|
|
||||||
t = Table(data, 10.5 * cm, 7.42 * cm)
|
|
||||||
elif len(options) <= 8:
|
|
||||||
t = Table(data, 10.5 * cm, 14.84 * cm)
|
|
||||||
else:
|
|
||||||
t = Table(data, 10.5 * cm, 29.7 * cm)
|
|
||||||
|
|
||||||
t.setStyle(TableStyle([
|
|
||||||
('GRID', (0, 0), (-1, -1), 0.25, colors.grey),
|
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP')]))
|
|
||||||
story.append(t)
|
|
||||||
|
@ -1,378 +0,0 @@
|
|||||||
import random
|
|
||||||
import re
|
|
||||||
from html import escape
|
|
||||||
|
|
||||||
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
|
|
||||||
from reportlab.lib import colors
|
|
||||||
from reportlab.lib.units import cm
|
|
||||||
from reportlab.platypus import PageBreak, Paragraph, Spacer, Table, TableStyle
|
|
||||||
|
|
||||||
from openslides.core.config import config
|
|
||||||
from openslides.utils.pdf import stylesheet
|
|
||||||
|
|
||||||
from .models import Category
|
|
||||||
|
|
||||||
|
|
||||||
def motions_to_pdf(pdf, motions):
|
|
||||||
"""
|
|
||||||
Create a PDF with all motions.
|
|
||||||
"""
|
|
||||||
motions = natsorted(motions, key=lambda motion: motion.identifier or '')
|
|
||||||
all_motion_cover(pdf, motions)
|
|
||||||
for motion in motions:
|
|
||||||
pdf.append(PageBreak())
|
|
||||||
motion_to_pdf(pdf, motion)
|
|
||||||
|
|
||||||
|
|
||||||
def motion_to_pdf(pdf, motion):
|
|
||||||
"""
|
|
||||||
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']))
|
|
||||||
|
|
||||||
motion_data = []
|
|
||||||
|
|
||||||
# submitter
|
|
||||||
cell1a = []
|
|
||||||
cell1a.append(Spacer(0, 0.2 * cm))
|
|
||||||
cell1a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" % _("Submitter"),
|
|
||||||
stylesheet['Heading4']))
|
|
||||||
cell1b = []
|
|
||||||
cell1b.append(Spacer(0, 0.2 * cm))
|
|
||||||
for submitter in motion.submitters.all():
|
|
||||||
cell1b.append(Paragraph(str(submitter), stylesheet['Normal']))
|
|
||||||
motion_data.append([cell1a, cell1b])
|
|
||||||
|
|
||||||
# TODO: choose this in workflow
|
|
||||||
if motion.state.allow_submitter_edit:
|
|
||||||
# Cell for the signature
|
|
||||||
cell2a = []
|
|
||||||
cell2b = []
|
|
||||||
cell2a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" %
|
|
||||||
_("Signature"), stylesheet['Heading4']))
|
|
||||||
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']:
|
|
||||||
cell3a = []
|
|
||||||
cell3b = []
|
|
||||||
cell3a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font><seqreset id='counter'>"
|
|
||||||
% _("Supporters"), stylesheet['Heading4']))
|
|
||||||
supporters = motion.supporters.all()
|
|
||||||
for supporter in supporters:
|
|
||||||
cell3b.append(Paragraph("<seq id='counter'/>. %s" % str(supporter),
|
|
||||||
stylesheet['Normal']))
|
|
||||||
cell3b.append(Spacer(0, 0.2 * cm))
|
|
||||||
motion_data.append([cell3a, cell3b])
|
|
||||||
|
|
||||||
# Motion state
|
|
||||||
cell4a = []
|
|
||||||
cell4b = []
|
|
||||||
cell4a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" % _("State"),
|
|
||||||
stylesheet['Heading4']))
|
|
||||||
cell4b.append(Paragraph(_(motion.state.name), stylesheet['Normal']))
|
|
||||||
motion_data.append([cell4a, cell4b])
|
|
||||||
|
|
||||||
# Version number
|
|
||||||
if motion.versions.count() > 1:
|
|
||||||
version = motion.get_active_version()
|
|
||||||
cell5a = []
|
|
||||||
cell5b = []
|
|
||||||
cell5a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" % _("Version"),
|
|
||||||
stylesheet['Heading4']))
|
|
||||||
cell5b.append(Paragraph("%s" % version.version_number, stylesheet['Normal']))
|
|
||||||
motion_data.append([cell5a, cell5b])
|
|
||||||
|
|
||||||
# voting result
|
|
||||||
polls = []
|
|
||||||
for poll in motion.polls.all():
|
|
||||||
if not poll.has_votes():
|
|
||||||
continue
|
|
||||||
polls.append(poll)
|
|
||||||
|
|
||||||
if polls:
|
|
||||||
cell6a = []
|
|
||||||
cell6b = []
|
|
||||||
cell6a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font>" %
|
|
||||||
_("Vote result"), stylesheet['Heading4']))
|
|
||||||
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" % (_("Valid votes"))
|
|
||||||
if poll.votesinvalid is not None:
|
|
||||||
invalid = "<br/>%s" % (_("Invalid votes"))
|
|
||||||
if poll.votescast is not None:
|
|
||||||
votescast = "<br/>%s" % (_("Votes cast"))
|
|
||||||
if len(polls) > 1:
|
|
||||||
cell6b.append(Paragraph("%s. %s" % (ballotcounter, _("Vote")),
|
|
||||||
stylesheet['Bold']))
|
|
||||||
cell6b.append(Paragraph(
|
|
||||||
"%s: %s <br/> %s: %s <br/> %s: %s <br/> %s %s %s" %
|
|
||||||
(_("Yes"), yes, _("No"), no, _("Abstain"), abstain, valid, invalid, votescast),
|
|
||||||
stylesheet['Normal']))
|
|
||||||
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),
|
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP')]))
|
|
||||||
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
|
|
||||||
if motion.reason:
|
|
||||||
pdf.append(Paragraph(_("Reason") + ":", stylesheet['Heading3']))
|
|
||||||
convert_html_to_reportlab(pdf, motion.reason)
|
|
||||||
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>•</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
|
|
||||||
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(' ', ' ')
|
|
||||||
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:
|
|
||||||
pdf.append(Paragraph(paragraph, stylesheet['InnerParagraph']))
|
|
||||||
|
|
||||||
|
|
||||||
def all_motion_cover(pdf, motions):
|
|
||||||
"""
|
|
||||||
Create a coverpage for all motions.
|
|
||||||
"""
|
|
||||||
pdf.append(Paragraph(escape(config["motions_export_title"]), stylesheet['Heading1']))
|
|
||||||
|
|
||||||
preamble = escape(config["motions_export_preamble"])
|
|
||||||
if preamble:
|
|
||||||
pdf.append(Paragraph("%s" % preamble.replace('\r\n', '<br/>'), stylesheet['Paragraph']))
|
|
||||||
|
|
||||||
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 %s" % (escape(category.prefix), escape(category.name)), stylesheet['Paragraph']))
|
|
||||||
if categories:
|
|
||||||
pdf.append(PageBreak())
|
|
||||||
|
|
||||||
# list of motions
|
|
||||||
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']))
|
|
||||||
|
|
||||||
|
|
||||||
def motion_poll_to_pdf(pdf, poll):
|
|
||||||
circle = "*" # = Unicode Character 'HEAVY LARGE CIRCLE' (U+2B55)
|
|
||||||
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>"
|
|
||||||
% (circle, _("Abstain")), stylesheet['Ballot_option']))
|
|
||||||
data = []
|
|
||||||
# get ballot papers config values
|
|
||||||
ballot_papers_selection = config["motions_pdf_ballot_papers_selection"]
|
|
||||||
ballot_papers_number = config["motions_pdf_ballot_papers_number"]
|
|
||||||
|
|
||||||
# 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
|
|
||||||
elif ballot_papers_selection == "NUMBER_OF_ALL_PARTICIPANTS":
|
|
||||||
number = int(get_user_model().objects.count())
|
|
||||||
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)
|
|
||||||
for user in range(int(number / 2)):
|
|
||||||
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)
|
|
@ -6,16 +6,4 @@ urlpatterns = [
|
|||||||
url(r'^docxtemplate/$',
|
url(r'^docxtemplate/$',
|
||||||
views.MotionDocxTemplateView.as_view(),
|
views.MotionDocxTemplateView.as_view(),
|
||||||
name='motions_docx_template'),
|
name='motions_docx_template'),
|
||||||
|
|
||||||
url(r'^pdf/$',
|
|
||||||
views.MotionPDFView.as_view(print_all_motions=True),
|
|
||||||
name='motions_pdf'),
|
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/pdf/$',
|
|
||||||
views.MotionPDFView.as_view(print_all_motions=False),
|
|
||||||
name='motions_single_pdf'),
|
|
||||||
|
|
||||||
url(r'^poll/(?P<poll_pk>\d+)/print/$',
|
|
||||||
views.MotionPollPDF.as_view(),
|
|
||||||
name='motionpoll_pdf'),
|
|
||||||
]
|
]
|
||||||
|
@ -3,10 +3,8 @@ import base64
|
|||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.text import slugify
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_noop
|
from django.utils.translation import ugettext_noop
|
||||||
from reportlab.platypus import SimpleDocTemplate
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
@ -20,7 +18,8 @@ from ..utils.rest_api import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
from ..utils.views import APIView, PDFView, SingleObjectMixin
|
from ..utils.views import APIView
|
||||||
|
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
@ -39,7 +38,6 @@ from .models import (
|
|||||||
State,
|
State,
|
||||||
Workflow,
|
Workflow,
|
||||||
)
|
)
|
||||||
from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
|
|
||||||
from .serializers import MotionPollSerializer
|
from .serializers import MotionPollSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -523,101 +521,6 @@ class WorkflowViewSet(ModelViewSet):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Views to generate PDFs and for the DOCX template
|
|
||||||
|
|
||||||
class MotionPollPDF(PDFView):
|
|
||||||
"""
|
|
||||||
Generates a ballotpaper.
|
|
||||||
"""
|
|
||||||
|
|
||||||
required_permission = 'motions.can_manage'
|
|
||||||
top_space = 0
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
self.poll = MotionPoll.objects.get(pk=self.kwargs['poll_pk'])
|
|
||||||
return super().get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_filename(self):
|
|
||||||
"""
|
|
||||||
Return the filename for the PDF.
|
|
||||||
"""
|
|
||||||
return u'%s_%s' % (_("Motion"), _("Vote"))
|
|
||||||
|
|
||||||
def get_template(self, buffer):
|
|
||||||
return SimpleDocTemplate(
|
|
||||||
buffer, topMargin=-6, bottomMargin=-6, leftMargin=0, rightMargin=0,
|
|
||||||
showBoundary=False)
|
|
||||||
|
|
||||||
def build_document(self, pdf_document, story):
|
|
||||||
pdf_document.build(story)
|
|
||||||
|
|
||||||
def append_to_pdf(self, pdf):
|
|
||||||
"""
|
|
||||||
Append PDF objects.
|
|
||||||
"""
|
|
||||||
motion_poll_to_pdf(pdf, self.poll)
|
|
||||||
|
|
||||||
|
|
||||||
class MotionPDFView(SingleObjectMixin, PDFView):
|
|
||||||
"""
|
|
||||||
Create the PDF for one or for all motions.
|
|
||||||
|
|
||||||
If self.print_all_motions is True, the view returns a PDF with all motions.
|
|
||||||
|
|
||||||
If self.print_all_motions is False, the view returns a PDF with only one
|
|
||||||
motion.
|
|
||||||
"""
|
|
||||||
model = Motion
|
|
||||||
top_space = 0
|
|
||||||
print_all_motions = False
|
|
||||||
|
|
||||||
def check_permission(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Checks if the requesting user has the permission to see the motion as
|
|
||||||
PDF.
|
|
||||||
"""
|
|
||||||
if self.print_all_motions:
|
|
||||||
is_allowed = request.user.has_perm('motions.can_see')
|
|
||||||
else:
|
|
||||||
is_allowed = self.get_object().get_allowed_actions(request.user)['see']
|
|
||||||
return is_allowed
|
|
||||||
|
|
||||||
def get_object(self, *args, **kwargs):
|
|
||||||
if self.print_all_motions:
|
|
||||||
obj = None
|
|
||||||
else:
|
|
||||||
obj = super().get_object(*args, **kwargs)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def get_filename(self):
|
|
||||||
"""
|
|
||||||
Return the filename for the PDF.
|
|
||||||
"""
|
|
||||||
if self.print_all_motions:
|
|
||||||
return _("Motions")
|
|
||||||
else:
|
|
||||||
if self.get_object().identifier:
|
|
||||||
suffix = self.get_object().identifier.replace(' ', '')
|
|
||||||
else:
|
|
||||||
suffix = self.get_object().title.replace(' ', '_')
|
|
||||||
suffix = slugify(suffix)
|
|
||||||
return '%s-%s' % (_("Motion"), suffix)
|
|
||||||
|
|
||||||
def append_to_pdf(self, pdf):
|
|
||||||
"""
|
|
||||||
Append PDF objects.
|
|
||||||
"""
|
|
||||||
if self.print_all_motions:
|
|
||||||
motions = []
|
|
||||||
for motion in Motion.objects.all():
|
|
||||||
if (not motion.state.required_permission_to_see or
|
|
||||||
self.request.user.has_perm(motion.state.required_permission_to_see)):
|
|
||||||
motions.append(motion)
|
|
||||||
motions_to_pdf(pdf, motions)
|
|
||||||
else:
|
|
||||||
motion_to_pdf(pdf, self.get_object())
|
|
||||||
|
|
||||||
|
|
||||||
class MotionDocxTemplateView(APIView):
|
class MotionDocxTemplateView(APIView):
|
||||||
"""
|
"""
|
||||||
Returns the template for motions docx export
|
Returns the template for motions docx export
|
||||||
|
@ -12,8 +12,6 @@ urlpatterns += [
|
|||||||
url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL.lstrip('/'), serve, {'document_root': settings.MEDIA_ROOT}),
|
url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL.lstrip('/'), serve, {'document_root': settings.MEDIA_ROOT}),
|
||||||
url(r'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/', permanent=True)),
|
url(r'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/', permanent=True)),
|
||||||
url(r'^rest/', include(router.urls)),
|
url(r'^rest/', include(router.urls)),
|
||||||
url(r'^agenda/', include('openslides.agenda.urls')),
|
|
||||||
url(r'^assignments/', include('openslides.assignments.urls')),
|
|
||||||
url(r'^motions/', include('openslides.motions.urls')),
|
url(r'^motions/', include('openslides.motions.urls')),
|
||||||
url(r'^users/', include('openslides.users.urls')),
|
url(r'^users/', include('openslides.users.urls')),
|
||||||
|
|
||||||
|
@ -188,14 +188,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
|||||||
# Return result
|
# Return result
|
||||||
return name
|
return name
|
||||||
|
|
||||||
# TODO: remove this function after PR#2476 is merged. (see Issue#2594)
|
|
||||||
def get_full_name(self):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
# TODO: remove this function after PR#2476 is merged. (see Issue#2594)
|
|
||||||
def get_short_name(self):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def get_search_index_string(self):
|
def get_search_index_string(self):
|
||||||
"""
|
"""
|
||||||
Returns a string that can be indexed for the search.
|
Returns a string that can be indexed for the search.
|
||||||
|
@ -19,13 +19,4 @@ urlpatterns = [
|
|||||||
url(r'^setpassword/$',
|
url(r'^setpassword/$',
|
||||||
views.SetPasswordView.as_view(),
|
views.SetPasswordView.as_view(),
|
||||||
name='user_setpassword'),
|
name='user_setpassword'),
|
||||||
|
|
||||||
# PDF
|
|
||||||
url(r'^print/$',
|
|
||||||
views.UsersListPDF.as_view(),
|
|
||||||
name='user_listpdf'),
|
|
||||||
|
|
||||||
url(r'^passwords/print/$',
|
|
||||||
views.UsersPasswordsPDF.as_view(),
|
|
||||||
name='user_passwordspdf'),
|
|
||||||
]
|
]
|
||||||
|
@ -3,7 +3,6 @@ from django.contrib.auth import logout as auth_logout
|
|||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy
|
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
@ -14,10 +13,9 @@ from ..utils.rest_api import (
|
|||||||
detail_route,
|
detail_route,
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
from ..utils.views import APIView, PDFView
|
from ..utils.views import APIView
|
||||||
from .access_permissions import UserAccessPermissions
|
from .access_permissions import UserAccessPermissions
|
||||||
from .models import Group, User
|
from .models import Group, User
|
||||||
from .pdf import users_passwords_to_pdf, users_to_pdf
|
|
||||||
from .serializers import GroupSerializer
|
from .serializers import GroupSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -251,38 +249,3 @@ class SetPasswordView(APIView):
|
|||||||
else:
|
else:
|
||||||
raise ValidationError({'detail': _('Old password does not match.')})
|
raise ValidationError({'detail': _('Old password does not match.')})
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Views to generate PDFs
|
|
||||||
|
|
||||||
class UsersListPDF(PDFView):
|
|
||||||
"""
|
|
||||||
Generate a list of all users as PDF.
|
|
||||||
"""
|
|
||||||
required_permission = 'users.can_see_extra_data'
|
|
||||||
filename = ugettext_lazy('user-list')
|
|
||||||
document_title = ugettext_lazy('List of users')
|
|
||||||
|
|
||||||
def append_to_pdf(self, pdf):
|
|
||||||
"""
|
|
||||||
Append PDF objects.
|
|
||||||
"""
|
|
||||||
users_to_pdf(pdf)
|
|
||||||
|
|
||||||
|
|
||||||
class UsersPasswordsPDF(PDFView):
|
|
||||||
"""
|
|
||||||
Generate the access data welcome paper for all users as PDF.
|
|
||||||
"""
|
|
||||||
required_permission = 'users.can_manage'
|
|
||||||
filename = ugettext_lazy('user-access-data')
|
|
||||||
top_space = 0
|
|
||||||
|
|
||||||
def build_document(self, pdf_document, story):
|
|
||||||
pdf_document.build(story)
|
|
||||||
|
|
||||||
def append_to_pdf(self, pdf):
|
|
||||||
"""
|
|
||||||
Append PDF objects.
|
|
||||||
"""
|
|
||||||
users_passwords_to_pdf(pdf)
|
|
||||||
|
@ -1,273 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from os.path import join as path_join
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import formats
|
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
from reportlab.lib import colors
|
|
||||||
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
|
||||||
from reportlab.lib.units import cm
|
|
||||||
from reportlab.pdfbase import pdfmetrics
|
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
from reportlab.rl_config import defaultPageSize
|
|
||||||
|
|
||||||
from openslides.core.config import config
|
|
||||||
|
|
||||||
# register new truetype fonts
|
|
||||||
pdfmetrics.registerFont(TTFont(
|
|
||||||
'Ubuntu', path_join(settings.MODULE_DIR, 'core', 'static', 'fonts', 'Ubuntu-R.ttf')))
|
|
||||||
pdfmetrics.registerFont(TTFont(
|
|
||||||
'Ubuntu-Bold', path_join(settings.MODULE_DIR, 'core', 'static', 'fonts', 'Ubuntu-B.ttf')))
|
|
||||||
pdfmetrics.registerFont(TTFont(
|
|
||||||
'Ubuntu-Italic', path_join(settings.MODULE_DIR, 'core', 'static', 'fonts', 'Ubuntu-RI.ttf')))
|
|
||||||
pdfmetrics.registerFont(TTFont(
|
|
||||||
'circlefont', path_join(settings.MODULE_DIR, 'core', 'static', 'fonts', 'circle.ttf')))
|
|
||||||
|
|
||||||
pdfmetrics.registerFontFamily('Ubuntu', normal='Ubuntu', bold='Ubuntu-Bold', italic='Ubuntu-Italic')
|
|
||||||
|
|
||||||
# set style information
|
|
||||||
PAGE_HEIGHT = defaultPageSize[1]
|
|
||||||
PAGE_WIDTH = defaultPageSize[0]
|
|
||||||
|
|
||||||
|
|
||||||
# set custom stylesheets
|
|
||||||
stylesheet = StyleSheet1()
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Normal',
|
|
||||||
fontName='Ubuntu',
|
|
||||||
fontSize=10,
|
|
||||||
leading=12,
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Paragraph',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
leading=14,
|
|
||||||
spaceAfter=15
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Paragraph12',
|
|
||||||
parent=stylesheet['Paragraph'],
|
|
||||||
fontSize=12
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='InnerParagraph',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
leading=14,
|
|
||||||
spaceBefore=5,
|
|
||||||
spaceAfter=5,
|
|
||||||
bulletIndent=-15,
|
|
||||||
bulletFontSize=8,
|
|
||||||
bulletColor=colors.grey
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='InnerListParagraph',
|
|
||||||
parent=stylesheet['InnerParagraph'],
|
|
||||||
bulletIndent=10,
|
|
||||||
bulletFontSize=10,
|
|
||||||
bulletColor=colors.black,
|
|
||||||
leftIndent=30
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='InnerMonotypeParagraph',
|
|
||||||
parent=stylesheet['InnerParagraph'],
|
|
||||||
fontName='Courier',
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='InnerH1Paragraph',
|
|
||||||
parent=stylesheet['InnerParagraph'],
|
|
||||||
fontName='Ubuntu-Bold',
|
|
||||||
fontSize=16,
|
|
||||||
spaceBefore=20,
|
|
||||||
spaceAfter=10,
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='InnerH2Paragraph',
|
|
||||||
parent=stylesheet['InnerH1Paragraph'],
|
|
||||||
fontSize=12,
|
|
||||||
spaceBefore=20,
|
|
||||||
spaceAfter=10,
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='InnerH3Paragraph',
|
|
||||||
parent=stylesheet['InnerH2Paragraph'],
|
|
||||||
fontSize=10,
|
|
||||||
spaceBefore=15,
|
|
||||||
spaceAfter=5,
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Small',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=8
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Italic',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontName='Ubuntu-Italic',
|
|
||||||
spaceAfter=5
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Bold',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontName='Ubuntu-Bold',
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Heading1',
|
|
||||||
parent=stylesheet['Bold'],
|
|
||||||
fontSize=24,
|
|
||||||
leading=30,
|
|
||||||
spaceAfter=6,
|
|
||||||
), alias='h1')
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Heading2',
|
|
||||||
parent=stylesheet['Bold'],
|
|
||||||
fontSize=14,
|
|
||||||
leading=24,
|
|
||||||
spaceAfter=10,
|
|
||||||
), alias='h2')
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Heading3',
|
|
||||||
parent=stylesheet['Bold'],
|
|
||||||
fontSize=12,
|
|
||||||
leading=20,
|
|
||||||
), alias='h3')
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Heading4',
|
|
||||||
parent=stylesheet['Bold'],
|
|
||||||
fontSize=10,
|
|
||||||
leading=20,
|
|
||||||
), alias='h4')
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Item',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=14,
|
|
||||||
leading=14,
|
|
||||||
leftIndent=0,
|
|
||||||
spaceAfter=15,
|
|
||||||
))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Subitem',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=10,
|
|
||||||
leading=10,
|
|
||||||
leftIndent=20,
|
|
||||||
spaceAfter=15))
|
|
||||||
stylesheet.add(ParagraphStyle(
|
|
||||||
name='Tablecell',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=9))
|
|
||||||
stylesheet.add(ParagraphStyle(name='Signaturefield',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
spaceBefore=15)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ballot stylesheets
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_title',
|
|
||||||
parent=stylesheet['Bold'],
|
|
||||||
fontSize=12,
|
|
||||||
leading=14,
|
|
||||||
leftIndent=30),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_subtitle',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=10,
|
|
||||||
leading=12,
|
|
||||||
leftIndent=30,
|
|
||||||
rightIndent=20,
|
|
||||||
spaceAfter=5),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_description',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=7,
|
|
||||||
leading=10,
|
|
||||||
leftIndent=30),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_option',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=12,
|
|
||||||
leading=24,
|
|
||||||
leftIndent=30),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_option_name_YNA',
|
|
||||||
parent=stylesheet['Ballot_option'],
|
|
||||||
leading=14),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_option_name',
|
|
||||||
parent=stylesheet['Ballot_option_name_YNA'],
|
|
||||||
leading=17),
|
|
||||||
)
|
|
||||||
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_option_suffix_YNA',
|
|
||||||
parent=stylesheet['Ballot_option_name_YNA'],
|
|
||||||
fontSize=8,
|
|
||||||
leading=11),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_option_suffix',
|
|
||||||
parent=stylesheet['Ballot_option_suffix_YNA'],
|
|
||||||
leading=16,
|
|
||||||
leftIndent=48),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='Ballot_option_circle_YNA',
|
|
||||||
parent=stylesheet['Ballot_option_name_YNA'],
|
|
||||||
leftIndent=48,
|
|
||||||
spaceAfter=18),
|
|
||||||
)
|
|
||||||
# Password paper stylesheets
|
|
||||||
stylesheet.add(ParagraphStyle(name='formfield',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontSize=12,
|
|
||||||
leading=18,
|
|
||||||
leftIndent=0),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='formfield_value',
|
|
||||||
parent=stylesheet['Normal'],
|
|
||||||
fontName='Courier',
|
|
||||||
fontSize=12,
|
|
||||||
leading=28,
|
|
||||||
leftIndent=10),
|
|
||||||
)
|
|
||||||
stylesheet.add(ParagraphStyle(name='qrcode_comment',
|
|
||||||
parent=stylesheet['Small'],
|
|
||||||
spaceBefore=6),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def firstPage(canvas, doc):
|
|
||||||
canvas.saveState()
|
|
||||||
# page header (with event information)
|
|
||||||
canvas.setFont('Ubuntu', 10)
|
|
||||||
canvas.setFillGray(0.4)
|
|
||||||
|
|
||||||
title_line = u"%s | %s" % (config["general_event_name"],
|
|
||||||
config["general_event_description"])
|
|
||||||
if len(title_line) > 75:
|
|
||||||
title_line = "%s ..." % title_line[:70]
|
|
||||||
canvas.drawString(2.75 * cm, 28 * cm, title_line)
|
|
||||||
if config["general_event_date"] and config["general_event_location"]:
|
|
||||||
canvas.drawString(2.75 * cm, 27.6 * cm, u"%s, %s"
|
|
||||||
% (config["general_event_date"], config["general_event_location"]))
|
|
||||||
|
|
||||||
# time
|
|
||||||
canvas.setFont('Ubuntu', 7)
|
|
||||||
time = formats.date_format(datetime.now(), 'DATETIME_FORMAT')
|
|
||||||
canvas.drawString(15 * cm, 28 * cm, _("As of: %s") % time)
|
|
||||||
|
|
||||||
# title
|
|
||||||
if doc.title:
|
|
||||||
canvas.setFont('Ubuntu-Bold', 24)
|
|
||||||
canvas.setFillGray(0)
|
|
||||||
canvas.drawString(2.75 * cm, PAGE_HEIGHT - 108, doc.title)
|
|
||||||
|
|
||||||
# footer (with page number)
|
|
||||||
canvas.setFont('Ubuntu', 8)
|
|
||||||
canvas.setFillGray(0.4)
|
|
||||||
canvas.drawString(10 * cm, 1 * cm, _("Page %s") % doc.page)
|
|
||||||
canvas.restoreState()
|
|
||||||
|
|
||||||
|
|
||||||
def laterPages(canvas, doc):
|
|
||||||
canvas.saveState()
|
|
||||||
# footer (with page number)
|
|
||||||
canvas.setFont('Ubuntu', 7)
|
|
||||||
canvas.setFillGray(0.4)
|
|
||||||
canvas.drawString(10 * cm, 1 * cm, _("Page %s") % doc.page)
|
|
||||||
canvas.restoreState()
|
|
@ -1,46 +1,11 @@
|
|||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.utils.translation import ugettext_lazy
|
|
||||||
from django.views import generic as django_views
|
from django.views import generic as django_views
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from reportlab.lib.units import cm
|
|
||||||
from reportlab.platypus import SimpleDocTemplate, Spacer
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView as _APIView
|
from rest_framework.views import APIView as _APIView
|
||||||
|
|
||||||
from .pdf import firstPage, laterPages
|
|
||||||
|
|
||||||
View = django_views.View
|
View = django_views.View
|
||||||
|
|
||||||
|
|
||||||
class SingleObjectMixin(django_views.detail.SingleObjectMixin):
|
|
||||||
"""
|
|
||||||
Mixin for single objects from the database.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def dispatch(self, *args, **kwargs):
|
|
||||||
if not hasattr(self, 'object'):
|
|
||||||
# Save the object not only in the cache but in the public
|
|
||||||
# attribute self.object because Django expects this later.
|
|
||||||
# Because get_object() has an internal cache this line is not a
|
|
||||||
# performance problem.
|
|
||||||
self.object = self.get_object()
|
|
||||||
return super().dispatch(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_object(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Returns the single object from database or cache.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
obj = self._object
|
|
||||||
except AttributeError:
|
|
||||||
obj = super().get_object(*args, **kwargs)
|
|
||||||
self._object = obj
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class CSRFMixin:
|
class CSRFMixin:
|
||||||
"""
|
"""
|
||||||
Adds the csrf cookie to the response.
|
Adds the csrf cookie to the response.
|
||||||
@ -52,76 +17,6 @@ class CSRFMixin:
|
|||||||
return ensure_csrf_cookie(view)
|
return ensure_csrf_cookie(view)
|
||||||
|
|
||||||
|
|
||||||
class PDFView(View):
|
|
||||||
"""
|
|
||||||
View to generate an PDF.
|
|
||||||
"""
|
|
||||||
filename = ugettext_lazy('undefined-filename')
|
|
||||||
top_space = 3
|
|
||||||
document_title = None
|
|
||||||
required_permission = None
|
|
||||||
|
|
||||||
def check_permission(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Checks if the user has the required permission.
|
|
||||||
"""
|
|
||||||
if self.required_permission is None:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return request.user.has_perm(self.required_permission)
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Check if the user has the permission.
|
|
||||||
|
|
||||||
If the user is not logged in, redirect the user to the login page.
|
|
||||||
"""
|
|
||||||
if not self.check_permission(request, *args, **kwargs):
|
|
||||||
raise PermissionDenied
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_top_space(self):
|
|
||||||
return self.top_space
|
|
||||||
|
|
||||||
def get_document_title(self):
|
|
||||||
if self.document_title:
|
|
||||||
return str(self.document_title)
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def get_filename(self):
|
|
||||||
return self.filename
|
|
||||||
|
|
||||||
def get_template(self, buffer):
|
|
||||||
return SimpleDocTemplate(buffer)
|
|
||||||
|
|
||||||
def build_document(self, pdf_document, story):
|
|
||||||
pdf_document.build(
|
|
||||||
story, onFirstPage=firstPage, onLaterPages=laterPages)
|
|
||||||
|
|
||||||
def render_to_response(self, filename):
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
|
||||||
filename = 'filename=%s.pdf;' % self.get_filename()
|
|
||||||
response['Content-Disposition'] = filename.encode('utf-8')
|
|
||||||
|
|
||||||
buffer = BytesIO()
|
|
||||||
pdf_document = self.get_template(buffer)
|
|
||||||
pdf_document.title = self.get_document_title()
|
|
||||||
story = [Spacer(1, self.get_top_space() * cm)]
|
|
||||||
|
|
||||||
self.append_to_pdf(story)
|
|
||||||
|
|
||||||
self.build_document(pdf_document, story)
|
|
||||||
|
|
||||||
pdf = buffer.getvalue()
|
|
||||||
buffer.close()
|
|
||||||
response.write(pdf)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
return self.render_to_response(self.get_filename())
|
|
||||||
|
|
||||||
|
|
||||||
class APIView(_APIView):
|
class APIView(_APIView):
|
||||||
"""
|
"""
|
||||||
The Django Rest framework APIView with improvements for OpenSlides.
|
The Django Rest framework APIView with improvements for OpenSlides.
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
# Requirements for OpenSlides in production in alphabetical order
|
# Requirements for OpenSlides in production in alphabetical order
|
||||||
Django>=1.10.1,<1.11
|
Django>=1.10.1,<1.11
|
||||||
beautifulsoup4>=4.5,<4.6
|
|
||||||
channels>=0.15,<1.0
|
channels>=0.15,<1.0
|
||||||
djangorestframework>=3.4,<3.5
|
djangorestframework>=3.4,<3.5
|
||||||
html5lib>=0.99,<1.0
|
|
||||||
jsonfield>=0.9.19,<1.1
|
jsonfield>=0.9.19,<1.1
|
||||||
natsort>=3.2,<5.1
|
|
||||||
PyPDF2>=1.25.0,<1.27
|
PyPDF2>=1.25.0,<1.27
|
||||||
reportlab>=3.0,<3.4
|
|
||||||
roman>=2.0,<2.1
|
roman>=2.0,<2.1
|
||||||
setuptools>=18.5,<28.0
|
setuptools>=18.5,<28.0
|
||||||
twisted>=16.2,<16.4
|
twisted>=16.2,<16.4
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
from openslides.topics.models import Topic
|
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestAgendaPDF(TestCase):
|
|
||||||
def test_get(self):
|
|
||||||
"""
|
|
||||||
Tests that a requst on the pdf-page returns with statuscode 200.
|
|
||||||
"""
|
|
||||||
Topic.objects.create(title='item1')
|
|
||||||
self.client.login(username='admin', password='admin')
|
|
||||||
|
|
||||||
response = self.client.get('/agenda/print/')
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
@ -1,25 +0,0 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.test import APIClient
|
|
||||||
|
|
||||||
from openslides.assignments.models import Assignment
|
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class PDF(TestCase):
|
|
||||||
"""
|
|
||||||
Tests assignment PDF.
|
|
||||||
"""
|
|
||||||
def setUp(self):
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.login(username='admin', password='admin')
|
|
||||||
self.admin = get_user_model().objects.get(username='admin')
|
|
||||||
self.assignment = Assignment.objects.create(title='test_assignment_OxieG7BioChahteY4aeM', open_posts=1)
|
|
||||||
|
|
||||||
def test_pdf_with_ballot(self):
|
|
||||||
self.assignment.set_candidate(self.admin)
|
|
||||||
self.assignment.create_poll()
|
|
||||||
self.assignment.polls.all()[0].set_published(True)
|
|
||||||
response = self.client.get(reverse('assignments_pdf'))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
@ -1,28 +0,0 @@
|
|||||||
from django.core.urlresolvers import reverse
|
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from openslides.core.config import config
|
|
||||||
from openslides.motions.models import Motion
|
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class AllMotionPDF(TestCase):
|
|
||||||
"""
|
|
||||||
Tests creating a PDF of all motions.
|
|
||||||
"""
|
|
||||||
def setUp(self):
|
|
||||||
self.client.login(username='admin', password='admin')
|
|
||||||
config['motions_identifier'] = 'manually'
|
|
||||||
self.motion = Motion(
|
|
||||||
title='test_title_Dik4jaey5ku6axee7Dai',
|
|
||||||
text='test_text_Auvie4euf2oang8ahcie')
|
|
||||||
self.motion.save()
|
|
||||||
self.motion2 = Motion(
|
|
||||||
title='test_title_AeTheech6euf9siM8uey',
|
|
||||||
text='test_text_Cohsh2egaexae8eebiot',
|
|
||||||
identifier='42')
|
|
||||||
self.motion2.save()
|
|
||||||
|
|
||||||
def test_pdf_all_motions(self):
|
|
||||||
response = self.client.get(reverse('motions_pdf'))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
@ -1,45 +0,0 @@
|
|||||||
from django.test.client import Client
|
|
||||||
|
|
||||||
from openslides.motions.models import Motion
|
|
||||||
from openslides.users.models import User
|
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class MotionPDFTest(TestCase):
|
|
||||||
"""
|
|
||||||
Tests for motion PDF.
|
|
||||||
"""
|
|
||||||
def setUp(self):
|
|
||||||
# Admin
|
|
||||||
self.admin = User.objects.get(pk=1)
|
|
||||||
self.admin_client = Client()
|
|
||||||
self.admin_client.login(username='admin', password='admin')
|
|
||||||
|
|
||||||
# Registered
|
|
||||||
self.registered = User.objects.create_user('registered', 'registered')
|
|
||||||
self.registered_client = Client()
|
|
||||||
self.registered_client.login(username='registered', password='registered')
|
|
||||||
|
|
||||||
def test_render_nested_list(self):
|
|
||||||
Motion.objects.create(
|
|
||||||
title='Test Title chieM6Aing8Eegh9ePhu',
|
|
||||||
text='<ul><li>Element 1 aKaesieze6mahR2ielie'
|
|
||||||
'<ul><li>Subelement 1 rel0liiGh0bi3ree6Jei</li>'
|
|
||||||
'<li>Subelement 2 rel0liiGh0bi3ree6Jei</li></ul></li>'
|
|
||||||
'<li>Element 2 rel0liiGh0bi3ree6Jei</li></ul>')
|
|
||||||
response = self.admin_client.get('/motions/1/pdf/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_get_without_required_permission_from_state(self):
|
|
||||||
motion = Motion.objects.create(title='motion_title_zthguis8qqespgknme52')
|
|
||||||
motion.state.required_permission_to_see = 'motions.can_manage'
|
|
||||||
motion.state.save()
|
|
||||||
response = self.registered_client.get('/motions/1/pdf/')
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
def test_get_with_filtered_motion_list(self):
|
|
||||||
motion = Motion.objects.create(title='motion_title_qwgvzf6487guni0oikcc')
|
|
||||||
motion.state.required_permission_to_see = 'motions.can_manage'
|
|
||||||
motion.state.save()
|
|
||||||
response = self.registered_client.get('/motions/pdf/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
@ -1,57 +1,9 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from openslides.utils import views
|
from openslides.utils import views
|
||||||
|
|
||||||
|
|
||||||
@patch('builtins.super')
|
|
||||||
class SingleObjectMixinTest(TestCase):
|
|
||||||
def test_get_object_cache(self, mock_super):
|
|
||||||
"""
|
|
||||||
Test that the method get_object caches his result.
|
|
||||||
|
|
||||||
Tests that get_object from the django view is only called once, even if
|
|
||||||
get_object on our class is called twice.
|
|
||||||
"""
|
|
||||||
view = views.SingleObjectMixin()
|
|
||||||
|
|
||||||
view.get_object()
|
|
||||||
view.get_object()
|
|
||||||
|
|
||||||
mock_super().get_object.assert_called_once_with()
|
|
||||||
|
|
||||||
def test_dispatch_with_existin_object(self, mock_super):
|
|
||||||
view = views.SingleObjectMixin()
|
|
||||||
view.object = 'old_object'
|
|
||||||
view.get_object = MagicMock()
|
|
||||||
|
|
||||||
view.dispatch()
|
|
||||||
|
|
||||||
mock_super().dispatch.assert_called_with()
|
|
||||||
self.assertEqual(
|
|
||||||
view.object,
|
|
||||||
'old_object',
|
|
||||||
"view.object should not be changed")
|
|
||||||
self.assertFalse(
|
|
||||||
view.get_object.called,
|
|
||||||
"view.get_object() should not be called")
|
|
||||||
|
|
||||||
def test_dispatch_without_existin_object(self, mock_super):
|
|
||||||
view = views.SingleObjectMixin()
|
|
||||||
view.get_object = MagicMock(return_value='new_object')
|
|
||||||
|
|
||||||
view.dispatch()
|
|
||||||
|
|
||||||
mock_super().dispatch.assert_called_with()
|
|
||||||
self.assertEqual(
|
|
||||||
view.object,
|
|
||||||
'new_object',
|
|
||||||
"view.object should be changed")
|
|
||||||
self.assertTrue(
|
|
||||||
view.get_object.called,
|
|
||||||
"view.get_object() should be called")
|
|
||||||
|
|
||||||
|
|
||||||
class TestAPIView(TestCase):
|
class TestAPIView(TestCase):
|
||||||
def test_class_creation(self):
|
def test_class_creation(self):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user