from morepath import redirect
from onegov.core.crypto import random_token
from onegov.core.security import Private
from onegov.core.security import Public
from onegov.core.static import StaticFile
from onegov.core.utils import normalize_for_url
from onegov.file.integration import render_depot_file
from onegov.file.utils import as_fileintent
from onegov.form import Form
from onegov.swissvotes import _
from onegov.swissvotes import SwissvotesApp
from onegov.swissvotes.forms import AttachmentsForm
from onegov.swissvotes.forms import AttachmentsSearchForm
from onegov.swissvotes.layouts import DeleteVoteAttachmentLayout
from onegov.swissvotes.layouts import DeleteVoteLayout
from onegov.swissvotes.layouts import ManageCampaingMaterialLayout
from onegov.swissvotes.layouts import ManageCampaingMaterialNayLayout
from onegov.swissvotes.layouts import ManageCampaingMaterialYeaLayout
from onegov.swissvotes.layouts import UploadVoteAttachemtsLayout
from onegov.swissvotes.layouts import VoteCampaignMaterialLayout
from onegov.swissvotes.layouts import VoteLayout
from onegov.swissvotes.layouts import VoteStrengthsLayout
from onegov.swissvotes.models import Actor
from onegov.swissvotes.models import SwissVote
from onegov.swissvotes.models import SwissVoteFile
from webob.exc import HTTPBadRequest
from webob.exc import HTTPNotFound
from webob.exc import HTTPUnsupportedMediaType
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
from decimal import Decimal
from depot.io.interfaces import StoredFile
from onegov.core.types import JSON_ro
from onegov.core.types import RenderData
from onegov.swissvotes.request import SwissvotesRequest
from webob import Response
@SwissvotesApp.form(
model=SwissVote,
permission=Public,
template='vote.pt',
form=AttachmentsSearchForm
)
[docs]
def view_vote(
self: SwissVote,
request: 'SwissvotesRequest',
form: AttachmentsSearchForm
) -> 'RenderData':
layout = VoteLayout(self, request)
query = request.session.query(SwissVote)
prev_vote = (
query.order_by(SwissVote.bfs_number.desc())
.filter(SwissVote.bfs_number < self.bfs_number).first()
)
next_vote = (
query.order_by(SwissVote.bfs_number.asc())
.filter(SwissVote.bfs_number > self.bfs_number).first()
)
if self.bfs_map_host:
request.content_security_policy.default_src |= {self.bfs_map_host}
form.action += '#search'
if not form.errors:
form.apply_model(self)
return {
'layout': layout,
'prev': prev_vote,
'next': next_vote,
'map_preview': request.link(StaticFile('images/map-preview.png')),
'posters': self.posters(request),
'form': form,
'search_results': layout.search_results
}
@SwissvotesApp.json(
model=SwissVote,
permission=Public,
name='percentages'
)
[docs]
def view_vote_percentages(
self: SwissVote,
request: 'SwissvotesRequest'
) -> 'JSON_ro':
def create(
text: str,
text_label: str | None = None,
percentage: 'Decimal | None' = None,
yeas_p: 'Decimal | None' = None,
nays_p: 'Decimal | None' = None,
yeas: 'Decimal | int | None' = None,
nays: 'Decimal | int | None' = None,
code: str | None = None,
yea_label: str | None = None,
nay_label: str | None = None,
none_label: str | None = None,
empty: bool = False
) -> dict[str, 'JSON_ro']:
translate = request.translate
result: dict[str, JSON_ro] = {
'text': translate(text),
'text_label': translate(text_label) if text_label else '',
'yea': 0.0,
'nay': 0.0,
'none': 0.0,
'yea_label': '',
'nay_label': '',
'none_label': '',
'empty': empty
}
default_yea_label = _('${x}% yea')
default_nay_label = _('${x}% nay')
yea_label_no_perc = _('${x} yea')
nay_label_no_per = _('${x} nay')
if self._legal_form == 5:
default_yea_label = _('${x}% for the popular initiative')
default_nay_label = _('${x}% for the counter-proposal')
yea_label_no_perc = _('${x} for the popular initiative')
nay_label_no_per = _('${x} for the counter-proposal')
if percentage is not None:
yea = round(float(percentage), 1)
nay = round(float(100 - percentage), 1)
yea_label = yea_label or default_yea_label
nay_label = nay_label or default_nay_label
result.update({
'yea': yea,
'nay': nay,
'yea_label': translate(_(yea_label, mapping={'x': yea})),
'nay_label': translate(_(nay_label, mapping={'x': nay})),
})
elif yeas_p is not None and nays_p is not None:
yea = round(float(yeas_p), 1)
nay = round(float(nays_p), 1)
none = round(float(100 - yeas_p - nays_p), 1)
yea_label = yea_label or default_yea_label
nay_label = nay_label or default_nay_label
none_label = none_label or _('${x}% none')
result.update({
'yea': yea,
'nay': nay,
'none': none,
'yea_label': translate(_(yea_label, mapping={'x': yea})),
'nay_label': translate(_(nay_label, mapping={'x': nay})),
'none_label': translate(_(none_label, mapping={'x': none}))
})
elif yeas is not None and nays is not None:
yea = round(float(100 * (yeas / (yeas + nays))), 1)
nay = round(float(100 * (nays / (yeas + nays))), 1)
yea_label = yea_label or yea_label_no_perc
nay_label = nay_label or nay_label_no_per
result.update({
'yea': yea,
'nay': nay,
'yea_label': translate(_(yea_label, mapping={'x': yeas})),
'nay_label': translate(_(nay_label, mapping={'x': nays})),
})
elif code is not None:
value = getattr(self, f'_{code}')
label = getattr(self, code)
if value in (1, 9):
result.update({
'yea': True,
'yea_label': translate(label)
})
if value in (0, 2, 8):
result.update({
'nay': True,
'nay_label': translate(label)
})
if value == 3:
result.update({
'none': True,
'none_label': translate(label)
})
return result
results = [
create(
_('People'),
percentage=self.result_people_yeas_p,
code='result_people_accepted'
),
create(
_('Cantons'),
yeas=self.result_cantons_yeas,
nays=self.result_cantons_nays,
code='result_cantons_accepted'
),
create('', empty=True),
create(
_('Federal Council'),
text_label=_('Position of the Federal Council'),
code='position_federal_council'
),
create(
_('National Council'),
yeas=self.position_national_council_yeas,
nays=self.position_national_council_nays,
code='position_national_council'
),
create(
_('Council of States'),
yeas=self.position_council_of_states_yeas,
nays=self.position_council_of_states_nays,
code='position_council_of_states'
),
create(
_('Party slogans'),
text_label=_('Recommendations by political parties'),
yeas_p=self.national_council_share_yeas,
nays_p=self.national_council_share_nays,
yea_label=_(
'Electoral shares of parties: '
'Parties recommending Yes ${x}%'
) if self._legal_form != 5 else _(
'Electoral shares of parties: '
'Parties preferring the initiative ${x}%'
),
nay_label=_(
'Electoral shares of parties: '
'Parties recommending No ${x}%'
) if self._legal_form != 5 else _(
'Electoral shares of parties: '
'Parties preferring the counter-proposal ${x}%'
),
none_label=_(
'Electoral shares of parties: neutral/unknown ${x}%'
)
)
]
results = [
result for result in results if any((
result['yea'], result['nay'], result['none'], result['empty']
))
]
return {
'results': results,
'title': self.title
}
@SwissvotesApp.html(
model=SwissVote,
permission=Public,
template='strengths.pt',
name='strengths'
)
[docs]
def view_vote_strengths(
self: SwissVote,
request: 'SwissvotesRequest'
) -> 'RenderData':
return {
'layout': VoteStrengthsLayout(self, request),
'Actor': Actor
}
@SwissvotesApp.html(
model=SwissVote,
permission=Public,
template='campaign_material.pt',
name='campaign-material'
)
[docs]
def view_vote_campaign_material(
self: SwissVote,
request: 'SwissvotesRequest'
) -> 'RenderData':
layout = VoteCampaignMaterialLayout(self, request)
files = sorted(
(
(file, layout.metadata(file.filename))
for file in self.campaign_material_other
),
key=lambda it: (it[1].get('order', 999), it[1].get('title', ''))
)
return {
'layout': layout,
'files': files
}
@SwissvotesApp.form(
model=SwissVote,
permission=Private,
form=AttachmentsForm,
template='form.pt',
name='upload'
)
[docs]
def upload_vote_attachments(
self: SwissVote,
request: 'SwissvotesRequest',
form: AttachmentsForm
) -> 'RenderData | Response':
if form.submitted(request):
form.update_model(self)
request.message(_('Attachments updated'), 'success')
return request.redirect(request.link(self))
if not form.errors:
form.apply_model(self)
layout = UploadVoteAttachemtsLayout(self, request)
return {
'layout': layout,
'form': form,
'button_text': _('Upload'),
'cancel': request.link(self)
}
@SwissvotesApp.form(
model=SwissVote,
permission=Private,
form=Form,
template='form.pt',
name='delete'
)
[docs]
def delete_vote(
self: SwissVote,
request: 'SwissvotesRequest',
form: Form
) -> 'RenderData | Response':
layout = DeleteVoteLayout(self, request)
if form.submitted(request):
request.session.delete(self)
request.message(_('Vote deleted'), 'success')
return request.redirect(layout.votes_url)
return {
'layout': layout,
'form': form,
'subtitle': self.title,
'message': _(
'Do you really want to delete "${item}"?',
mapping={'item': self.title}
),
'button_text': _('Delete'),
'button_class': 'alert',
'cancel': request.link(self)
}
@SwissvotesApp.view(
model=SwissVoteFile,
render=render_depot_file,
permission=Public
)
[docs]
def view_file(
self: SwissVoteFile,
request: 'SwissvotesRequest'
) -> 'StoredFile':
@request.after
def set_filename(response: 'Response') -> None:
attribute = SwissVote.localized_files().get(self.name.split('-')[0])
if attribute:
bfs_number = self.linked_swissvotes[0].bfs_number
extension = attribute.extension
title = normalize_for_url(request.translate(attribute.label))
response.headers['Content-Disposition'] = (
f'inline; filename={bfs_number}-{title}.{extension}'
)
return self.reference.file
[docs]
def create_static_file_view(
attribute: str,
locale: str
) -> 'Callable[[SwissVote, SwissvotesRequest], Response]':
def static_view(
self: SwissVote,
request: 'SwissvotesRequest'
) -> 'Response':
file = self.get_file(attribute, locale=locale, fallback=False)
if not file:
raise HTTPNotFound()
return request.redirect(request.link(file))
return static_view
for attribute_name, attribute in SwissVote.localized_files().items():
for locale, view_name in attribute.static_views.items():
SwissvotesApp.view(
model=SwissVote,
permission=Public,
name=view_name
)(create_static_file_view(attribute_name, locale))
@SwissvotesApp.html(
model=SwissVote,
template='attachments.pt',
name='manage-campaign-material',
permission=Private
)
[docs]
def view_manage_campaign_material(
self: SwissVote,
request: 'SwissvotesRequest'
) -> 'RenderData':
layout = ManageCampaingMaterialLayout(self, request)
return {
'layout': layout,
'title': self.title,
'upload_url': layout.csrf_protected_url(
request.link(self, name='upload-campaign-material')
),
'files': self.campaign_material_other,
'notice': self,
}
@SwissvotesApp.view(
model=SwissVote,
name='upload-campaign-material',
permission=Private,
request_method='POST'
)
[docs]
def upload_manage_campaign_material(
self: SwissVote,
request: 'SwissvotesRequest'
) -> None:
request.assert_valid_csrf_token()
fs = request.params.get('file', '')
if isinstance(fs, str):
# malformed formdata
raise HTTPBadRequest()
attachment = SwissVoteFile(id=random_token())
attachment.name = f'campaign_material_other-{fs.filename}'
attachment.reference = as_fileintent(fs.file, fs.filename)
if attachment.reference.content_type != 'application/pdf':
raise HTTPUnsupportedMediaType()
self.files.append(attachment)
@SwissvotesApp.html(
model=SwissVote,
template='attachments.pt',
name='manage-campaign-material-yea',
permission=Private
)
[docs]
def view_manage_campaign_material_yea(
self: SwissVote,
request: 'SwissvotesRequest'
) -> 'RenderData':
layout = ManageCampaingMaterialYeaLayout(self, request)
return {
'layout': layout,
'title': self.title,
'upload_url': layout.csrf_protected_url(
request.link(self, name='upload-campaign-material-yea')
),
'files': self.campaign_material_yea,
'notice': self,
}
@SwissvotesApp.view(
model=SwissVote,
name='upload-campaign-material-yea',
permission=Private,
request_method='POST'
)
[docs]
def upload_manage_campaign_material_yea(
self: SwissVote,
request: 'SwissvotesRequest'
) -> None:
request.assert_valid_csrf_token()
fs = request.params.get('file', '')
if isinstance(fs, str):
# malformed formdata
raise HTTPBadRequest()
attachment = SwissVoteFile(id=random_token())
attachment.name = f'campaign_material_yea-{fs.filename}'
attachment.reference = as_fileintent(fs.file, fs.filename)
if attachment.reference.content_type not in ('image/jpeg', 'image/png'):
raise HTTPUnsupportedMediaType()
self.files.append(attachment)
@SwissvotesApp.html(
model=SwissVote,
template='attachments.pt',
name='manage-campaign-material-nay',
permission=Private
)
[docs]
def view_manage_campaign_material_nay(
self: SwissVote,
request: 'SwissvotesRequest'
) -> 'RenderData':
layout = ManageCampaingMaterialNayLayout(self, request)
return {
'layout': layout,
'title': self.title,
'upload_url': layout.csrf_protected_url(
request.link(self, name='upload-campaign-material-nay')
),
'files': self.campaign_material_nay,
'notice': self,
}
@SwissvotesApp.view(
model=SwissVote,
name='upload-campaign-material-nay',
permission=Private,
request_method='POST'
)
[docs]
def upload_manage_campaign_material_nay(
self: SwissVote,
request: 'SwissvotesRequest'
) -> None:
request.assert_valid_csrf_token()
fs = request.params.get('file', '')
if isinstance(fs, str):
# malformed formdata
raise HTTPBadRequest()
attachment = SwissVoteFile(id=random_token())
attachment.name = f'campaign_material_nay-{fs.filename}'
attachment.reference = as_fileintent(fs.file, fs.filename)
if attachment.reference.content_type not in ('image/jpeg', 'image/png'):
raise HTTPUnsupportedMediaType()
self.files.append(attachment)
@SwissvotesApp.form(
model=SwissVoteFile,
name='delete',
template='form.pt',
permission=Private,
form=Form
)
[docs]
def delete_vote_attachment(
self: SwissVoteFile,
request: 'SwissvotesRequest',
form: Form
) -> 'RenderData | Response':
layout = DeleteVoteAttachmentLayout(self, request)
name = 'manage-campaign-material'
if 'yea' in self.name:
name += '-yea'
if 'nay' in self.name:
name += '-nay'
url = request.link(layout.parent, name)
if form.submitted(request):
request.session.delete(self)
request.message(_('Attachment deleted.'), 'success')
return redirect(url)
return {
'message': _(
'Do you really want to delete "${item}"?',
mapping={'item': self.filename}
),
'layout': layout,
'form': form,
'title': self.filename,
'subtitle': _('Delete'),
'button_text': _('Delete'),
'button_class': 'alert',
'cancel': url
}