from cgi import escape 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 (PageBreak, Paragraph, SimpleDocTemplate, Spacer, LongTable, Table, TableStyle) from openslides.config.api import config from openslides.users.models import Group, User # TODO: remove this from openslides.utils.pdf import stylesheet from openslides.utils.rest_api import ( DestroyModelMixin, GenericViewSet, ModelViewSet, Response, UpdateModelMixin, ValidationError, detail_route) from openslides.utils.views import PDFView from .models import Assignment, AssignmentPoll from .serializers import ( AssignmentAllPollSerializer, AssignmentFullSerializer, AssignmentShortSerializer ) class AssignmentViewSet(ModelViewSet): """ API endpoint to list, retrieve, create, update and destroy assignments and to manage candidatures. """ queryset = Assignment.objects.all() def check_permissions(self, request): """ Calls self.permission_denied() if the requesting user has not the permission to see assignments and in case of create, update, partial_update or destroy requests the permission to manage assignments. """ if (not request.user.has_perm('assignments.can_see') or (self.action in ('create', 'update', 'partial_update', 'destroy') and not request.user.has_perm('assignments.can_manage'))): self.permission_denied(request) def get_serializer_class(self): """ Returns different serializer classes according to users permissions. """ if self.request.user.has_perm('assignments.can_manage'): serializer_class = AssignmentFullSerializer else: serializer_class = AssignmentShortSerializer return serializer_class @detail_route(methods=['post', 'delete']) def candidature_self(self, request, pk=None): """ View to nominate self as candidate (POST) or withdraw own candidature (DELETE). """ if not request.user.has_perm('assignments.can_nominate_self'): self.permission_denied(request) assignment = self.get_object() if assignment.is_elected(request.user): raise ValidationError({'detail': _('You are already elected.')}) if request.method == 'POST': message = self.nominate_self(request, assignment) else: # request.method == 'DELETE' message = self.withdraw_self(request, assignment) return Response({'detail': message}) def nominate_self(self, request, assignment): if assignment.phase == assignment.PHASE_FINISHED: raise ValidationError({'detail': _('You can not candidate to this election because it is finished.')}) if assignment.phase == assignment.PHASE_VOTING and not request.user.has_perm('assignments.can_manage'): # To nominate self during voting you have to be a manager. self.permission_denied(request) # If the request.user is already a candidate he can nominate himself nevertheless. assignment.set_candidate(request.user) return _('You were nominated successfully.') def withdraw_self(self, request, assignment): # Withdraw candidature and set self blocked. if assignment.phase == assignment.PHASE_FINISHED: raise ValidationError({'detail': _('You can not withdraw your candidature to this election because it is finished.')}) if assignment.phase == assignment.PHASE_VOTING and not request.user.has_perm('assignments.can_manage'): # To withdraw self during voting you have to be a manager. self.permission_denied(request) if not assignment.is_candidate(request.user): raise ValidationError({'detail': _('You are not a candidate of this election.')}) assignment.set_blocked(request.user) return _( 'You have withdrawn your candidature successfully. ' 'You can not be nominated by other participants anymore.') def get_user_from_request_data(self, request): """ Helper method to get a specific user from request data (not the request.user) so that the views self.candidature_other or self.mark_elected can play with him. """ if not isinstance(request.data, dict): detail = _('Invalid data. Expected dictionary, got %s.') % type(request.data) raise ValidationError({'detail': detail}) user_str = request.data.get('user', '') try: user_pk = int(user_str) except ValueError: raise ValidationError({'detail': _('Invalid data. Expected something like {"user": }.')}) try: user = User.objects.get(pk=user_pk) except User.DoesNotExist: raise ValidationError({'detail': _('Invalid data. User %d does not exist.') % user_pk}) return user @detail_route(methods=['post', 'delete']) def candidature_other(self, request, pk=None): """ View to nominate other users (POST) or delete their candidature status (DELETE). The client has to send {'user': }. """ if not request.user.has_perm('assignments.can_nominate_other'): self.permission_denied(request) user = self.get_user_from_request_data(request) assignment = self.get_object() if assignment.is_elected(user): raise ValidationError({'detail': _('User %s is already elected.') % user}) if request.method == 'POST': message = self.nominate_other(request, user, assignment) else: # request.method == 'DELETE' message = self.delete_other(request, user, assignment) return Response({'detail': message}) def nominate_other(self, request, user, assignment): if assignment.phase == assignment.PHASE_FINISHED: detail = _('You can not nominate someone to this election because it is finished.') raise ValidationError({'detail': detail}) if assignment.phase == assignment.PHASE_VOTING and not request.user.has_perm('assignments.can_manage'): # To nominate other during voting you have to be a manager. self.permission_denied(request) if not request.user.has_perm('assignments.can_manage'): if assignment.is_blocked(user): raise ValidationError({'detail': _('User %s does not want to be an candidate.') % user}) if assignment.is_elected(user): raise ValidationError({'detail': _('User %s is already elected.') % user}) # If the user is already a candidate he can be nominated nevertheless. assignment.set_candidate(user) return _('User %s was nominated successfully.') % user def delete_other(self, request, user, assignment): # To delete candidature status you have to be a manager. if not request.user.has_perm('assignments.can_manage'): self.permission_denied(request) if assignment.phase == assignment.PHASE_FINISHED: detail = _('You can not delete someones candidature to this election because it is finished.') raise ValidationError({'detail': detail}) if not assignment.is_candidate(user) and not assignment.is_blocked(user): raise ValidationError({'detail': _('User %s has no status in this election.') % user}) assignment.delete_related_user(user) return _('Candidate %s was withdrawn/unblocked successfully.') % user @detail_route(methods=['post', 'delete']) def mark_elected(self, request, pk=None): """ View to mark other users as elected (POST) or undo this (DELETE). The client has to send {'user': }. """ if not request.user.has_perm('assignments.can_manage'): self.permission_denied(request) user = self.get_user_from_request_data(request) assignment = self.get_object() if request.method == 'POST': if not assignment.is_candidate(user): raise ValidationError({'detail': _('User %s is not a candidate of this election.') % user}) assignment.set_elected(user) message = _('User %s was successfully elected.') % user else: # request.method == 'DELETE' if not assignment.is_elected(user): detail = _('User %s is not an elected candidate of this election.') % user raise ValidationError({'detail': detail}) assignment.set_candidate(user) message = _('User %s was successfully unelected.') % user return Response({'detail': message}) @detail_route(methods=['post']) def create_poll(self, request, pk=None): """ View to create a poll. It is a POST request without any data. """ if not request.user.has_perm('assignments.can_manage'): self.permission_denied(request) assignment = self.get_object() if not assignment.candidates.exists(): raise ValidationError({'detail': _('Can not create poll because there are no candidates.')}) with transaction.atomic(): assignment.create_poll() return Response({'detail': _(' Poll created successfully.')}) class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): """ API endpoint to update and destroy assignment polls. """ queryset = AssignmentPoll.objects.all() serializer_class = AssignmentAllPollSerializer def check_permissions(self, request): """ Calls self.permission_denied() if the requesting user has not the permission to see assignments and to manage assignments. """ if (not request.user.has_perm('assignments.can_see') or not request.user.has_perm('assignments.can_manage')): self.permission_denied(request) 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' % ( _("Assignment"), 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["assignment_pdf_title"]) story.append(Paragraph(title, stylesheet['Heading1'])) preamble = escape(config["assignment_pdf_preamble"]) if preamble: story.append(Paragraph( "%s" % preamble.replace('\r\n', '
'), stylesheet['Paragraph'])) story.append(Spacer(0, 0.75 * cm)) assignments = Assignment.objects.all() if not assignments: # No assignments existing story.append(Paragraph( _("No assignments 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:" % _("Candidates"), stylesheet['Heading4']), []]) for candidate in assignment.candidates: data.append([ [], Paragraph(".  %s" % candidate, stylesheet['Signaturefield'])]) if assignment.phase == assignment.PHASE_SEARCH: for x in range(0, 7): data.append([ [], Paragraph(".  " "__________________________________________", 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.iteritems(): row = [] candidate_string = candidate.clean_name if candidate in elected_candidates: candidate_string = "* " + candidate_string if candidate.name_suffix and length < 20: candidate_string += "\n(%s)" % candidate.name_suffix 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 '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.print_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.print_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.print_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', '
'), 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["assignment_pdf_ballot_papers_selection"] ballot_papers_number = config["assignment_pdf_ballot_papers_number"] # set number of ballot papers if ballot_papers_selection == "NUMBER_OF_DELEGATES": try: if Group.objects.get(pk=3): number = User.objects.filter(groups__pk=3).count() except Group.DoesNotExist: number = 0 elif ballot_papers_selection == "NUMBER_OF_ALL_PARTICIPANTS": number = int(User.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 or Yes) if self.poll.yesnoabstain: # YesNoAbstain 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'])) else: cell.append(Paragraph( " ", stylesheet['Ballot_option_suffix_YNA'])) cell.append(Paragraph("%(circle)s \ %(yes)s     \ %(circle)s \ %(no)s     \ %(circle)s \ %(abstain)s" % {'circle': circle, 'yes': _("Yes"), 'no': _("No"), 'abstain': _("Abstention")}, 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("%s \ %s" % (circle, candidate.clean_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)