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)
|
||||
<https://www.python.org/>`_ on your system. You also need the Python
|
||||
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
|
||||
<https://www.python.org/>`_ on your system.
|
||||
|
||||
|
||||
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>`_,
|
||||
License: Python Software Foundation License
|
||||
|
||||
* `Beautiful Soup <http://www.crummy.com/software/BeautifulSoup/>`_,
|
||||
License: MIT
|
||||
|
||||
* `Django <https://www.djangoproject.com>`_, License: BSD
|
||||
|
||||
* `Django REST framework <http://www.django-rest-framework.org>`_, License:
|
||||
BSD
|
||||
|
||||
* `html5lib <https://github.com/html5lib/html5lib-python>`_, License: MIT
|
||||
|
||||
* `Django Channels <https://github.com/andrewgodwin/channels/>`_, License: MIT
|
||||
|
||||
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_,
|
||||
License: MIT
|
||||
|
||||
* `natsort <https://pypi.python.org/pypi/natsort>`_, License: MIT
|
||||
|
||||
* `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
|
||||
|
||||
* `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.db import transaction
|
||||
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.utils.exceptions import OpenSlidesError
|
||||
from openslides.utils.pdf import stylesheet
|
||||
from openslides.utils.rest_api import (
|
||||
GenericViewSet,
|
||||
ListModelMixin,
|
||||
@ -19,7 +14,6 @@ from openslides.utils.rest_api import (
|
||||
detail_route,
|
||||
list_route,
|
||||
)
|
||||
from openslides.utils.views import PDFView
|
||||
|
||||
from .access_permissions import ItemAccessPermissions
|
||||
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'])
|
||||
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.db import transaction
|
||||
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 (
|
||||
DestroyModelMixin,
|
||||
GenericViewSet,
|
||||
@ -28,7 +11,6 @@ from openslides.utils.rest_api import (
|
||||
ValidationError,
|
||||
detail_route,
|
||||
)
|
||||
from openslides.utils.views import PDFView
|
||||
|
||||
from .access_permissions import AssignmentAccessPermissions
|
||||
from .models import Assignment, AssignmentPoll
|
||||
@ -220,365 +202,3 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
|
||||
"""
|
||||
return (self.request.user.has_perm('assignments.can_see') and
|
||||
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/$',
|
||||
views.MotionDocxTemplateView.as_view(),
|
||||
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.db import IntegrityError, transaction
|
||||
from django.http import Http404
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_noop
|
||||
from reportlab.platypus import SimpleDocTemplate
|
||||
from rest_framework import status
|
||||
|
||||
from ..core.config import config
|
||||
@ -20,7 +18,8 @@ from ..utils.rest_api import (
|
||||
ValidationError,
|
||||
detail_route,
|
||||
)
|
||||
from ..utils.views import APIView, PDFView, SingleObjectMixin
|
||||
from ..utils.views import APIView
|
||||
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
@ -39,7 +38,6 @@ from .models import (
|
||||
State,
|
||||
Workflow,
|
||||
)
|
||||
from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
|
||||
from .serializers import MotionPollSerializer
|
||||
|
||||
|
||||
@ -523,101 +521,6 @@ class WorkflowViewSet(ModelViewSet):
|
||||
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):
|
||||
"""
|
||||
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'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/', permanent=True)),
|
||||
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'^users/', include('openslides.users.urls')),
|
||||
|
||||
|
@ -188,14 +188,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
||||
# Return result
|
||||
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):
|
||||
"""
|
||||
Returns a string that can be indexed for the search.
|
||||
|
@ -19,13 +19,4 @@ urlpatterns = [
|
||||
url(r'^setpassword/$',
|
||||
views.SetPasswordView.as_view(),
|
||||
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.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
from ..core.config import config
|
||||
from ..utils.rest_api import (
|
||||
@ -14,10 +13,9 @@ from ..utils.rest_api import (
|
||||
detail_route,
|
||||
status,
|
||||
)
|
||||
from ..utils.views import APIView, PDFView
|
||||
from ..utils.views import APIView
|
||||
from .access_permissions import UserAccessPermissions
|
||||
from .models import Group, User
|
||||
from .pdf import users_passwords_to_pdf, users_to_pdf
|
||||
from .serializers import GroupSerializer
|
||||
|
||||
|
||||
@ -251,38 +249,3 @@ class SetPasswordView(APIView):
|
||||
else:
|
||||
raise ValidationError({'detail': _('Old password does not match.')})
|
||||
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.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.views import APIView as _APIView
|
||||
|
||||
from .pdf import firstPage, laterPages
|
||||
|
||||
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:
|
||||
"""
|
||||
Adds the csrf cookie to the response.
|
||||
@ -52,76 +17,6 @@ class CSRFMixin:
|
||||
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):
|
||||
"""
|
||||
The Django Rest framework APIView with improvements for OpenSlides.
|
||||
|
@ -1,13 +1,9 @@
|
||||
# Requirements for OpenSlides in production in alphabetical order
|
||||
Django>=1.10.1,<1.11
|
||||
beautifulsoup4>=4.5,<4.6
|
||||
channels>=0.15,<1.0
|
||||
djangorestframework>=3.4,<3.5
|
||||
html5lib>=0.99,<1.0
|
||||
jsonfield>=0.9.19,<1.1
|
||||
natsort>=3.2,<5.1
|
||||
PyPDF2>=1.25.0,<1.27
|
||||
reportlab>=3.0,<3.4
|
||||
roman>=2.0,<2.1
|
||||
setuptools>=18.5,<28.0
|
||||
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.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
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):
|
||||
def test_class_creation(self):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user