Source code for election_day.forms.election
from __future__ import annotations
from datetime import date
from onegov.core.utils import Bunch
from onegov.core.utils import normalize_for_url
from onegov.election_day import _
from onegov.election_day.layouts import DefaultLayout
from onegov.election_day.models import Election
from onegov.election_day.models import ElectionCompound
from onegov.election_day.models import ElectionRelationship
from onegov.form import Form
from onegov.form.fields import ChosenSelectField
from onegov.form.fields import ChosenSelectMultipleField
from onegov.form.fields import PanelField
from onegov.form.fields import UploadField
from onegov.form.validators import FileSizeLimit
from onegov.form.validators import WhitelistedMimeType
from re import findall
from sqlalchemy import or_
from wtforms.fields import BooleanField
from wtforms.fields import DateField
from wtforms.fields import IntegerField
from wtforms.fields import RadioField
from wtforms.fields import StringField
from wtforms.fields import TextAreaField
from wtforms.fields import URLField
from wtforms.validators import InputRequired
from wtforms.validators import NumberRange
from wtforms.validators import Optional
from wtforms.validators import URL
from wtforms.validators import ValidationError
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from onegov.election_day.request import ElectionDayRequest
from wtforms.fields.choices import _Choice
[docs]
class ElectionForm(Form):
[docs]
id_hint = PanelField(
label=_('Identifier'),
fieldset=_('Identifier'),
kind='callout',
text=_(
'The ID is used in the URL and might be used somewhere. '
'Changing the ID might break links on external sites!'
)
)
[docs]
id = StringField(
label=_('Identifier'),
fieldset=_('Identifier'),
validators=[
InputRequired()
],
)
[docs]
external_id = StringField(
label=_('External ID'),
fieldset=_('Identifier'),
render_kw={'long_description': _('Used for import if set.')},
)
[docs]
type = RadioField(
label=_('System'),
fieldset=_('Properties'),
choices=[
('majorz', _('Election based on the simple majority system')),
('proporz', _('Election based on proportional representation')),
],
validators=[
InputRequired()
],
default='majorz'
)
[docs]
majority_type = RadioField(
label=_('Majority Type'),
fieldset=_('Properties'),
choices=[
('absolute', _('Absolute')),
('relative', _('Relative')),
],
default='absolute',
validators=[
InputRequired()
],
depends_on=('type', 'majorz'),
)
[docs]
absolute_majority = IntegerField(
label=_('Absolute majority'),
fieldset=_('Properties'),
validators=[
Optional(),
NumberRange(min=1)
],
depends_on=('majority_type', 'absolute', 'type', 'majorz'),
)
[docs]
domain = RadioField(
label=_('Domain'),
fieldset=_('Properties'),
validators=[
InputRequired()
]
)
[docs]
region = ChosenSelectField(
label=_('District'),
fieldset=_('Properties'),
validators=[
InputRequired()
],
depends_on=('domain', 'region'),
)
[docs]
district = ChosenSelectField(
label=_('District'),
fieldset=_('Properties'),
validators=[
InputRequired()
],
depends_on=('domain', 'district'),
)
[docs]
municipality = ChosenSelectField(
label=_('Municipality'),
fieldset=_('Properties'),
validators=[
InputRequired()
],
depends_on=('domain', 'municipality'),
)
[docs]
tacit = BooleanField(
label=_('Tacit election'),
fieldset=_('Properties'),
render_kw={'force_simple': True}
)
[docs]
has_expats = BooleanField(
label=_('Expats are listed separately'),
fieldset=_('Properties'),
description=_(
'Expats are uploaded and listed as a separate row in the results. '
'Changing this option requires a new upload of the data.'
),
render_kw={'force_simple': True}
)
[docs]
date = DateField(
label=_('Date'),
fieldset=_('Properties'),
validators=[
InputRequired()
],
default=date.today
)
[docs]
shortcode = StringField(
label=_('Shortcode'),
fieldset=_('Properties'),
render_kw={'long_description': _('Used for sorting.')}
)
[docs]
mandates = IntegerField(
label=_('Mandates / Seats'),
fieldset=_('Properties'),
validators=[
InputRequired(),
NumberRange(min=1)
]
)
[docs]
voters_counts = BooleanField(
label=_('Voters counts'),
fieldset=_('View options'),
description=_(
'Shows voters counts instead of votes in the party strengths '
'view.'
),
depends_on=('type', 'proporz'),
)
[docs]
exact_voters_counts = BooleanField(
label=_('Exact voters counts'),
fieldset=_('View options'),
description=_(
'Shows exact voters counts instead of rounded values.'
),
depends_on=('type', 'proporz'),
render_kw={'force_simple': True}
)
[docs]
horizontal_party_strengths = BooleanField(
label=_('Horizonal party strengths chart'),
fieldset=_('View options'),
description=_(
'Shows a horizontal bar chart instead of a vertical bar chart.'
),
depends_on=('type', 'proporz', 'show_party_strengths', 'y'),
render_kw={'force_simple': True}
)
[docs]
use_historical_party_results = BooleanField(
label=_('Use party results from the last legislative period'),
fieldset=_('View options'),
description=_(
'Requires party results. Requires a related election from another '
'legislative period with party results. Requires that both '
'elections use the same party IDs.'
),
depends_on=('type', 'proporz'),
render_kw={'force_simple': True}
)
[docs]
show_party_strengths = BooleanField(
label=_('Party strengths'),
description=_(
'Shows a tab with the comparison of party strengths as a bar '
'chart. Requires party results.'
),
fieldset=_('Views'),
depends_on=('type', 'proporz'),
render_kw={'force_simple': True}
)
[docs]
show_party_panachage = BooleanField(
label=_('Panachage (parties)'),
description=_(
'Shows a tab with the panachage. Requires party results.'
),
fieldset=_('Views'),
depends_on=('type', 'proporz'),
render_kw={'force_simple': True}
)
[docs]
title_de = StringField(
label=_('German'),
fieldset=_('Title of the election'),
render_kw={'lang': 'de'}
)
[docs]
title_fr = StringField(
label=_('French'),
fieldset=_('Title of the election'),
render_kw={'lang': 'fr'}
)
[docs]
title_it = StringField(
label=_('Italian'),
fieldset=_('Title of the election'),
render_kw={'lang': 'it'}
)
[docs]
title_rm = StringField(
label=_('Romansh'),
fieldset=_('Title of the election'),
render_kw={'lang': 'rm'}
)
[docs]
short_title_de = StringField(
label=_('German'),
fieldset=_('Short title of the election'),
render_kw={'lang': 'de'}
)
[docs]
short_title_fr = StringField(
label=_('French'),
fieldset=_('Short title of the election'),
render_kw={'lang': 'fr'}
)
[docs]
short_title_it = StringField(
label=_('Italian'),
fieldset=_('Short title of the election'),
render_kw={'lang': 'it'}
)
[docs]
short_title_rm = StringField(
label=_('Romansh'),
fieldset=_('Short title of the election'),
render_kw={'lang': 'rm'}
)
[docs]
explanations_pdf = UploadField(
label=_('Explanations (PDF)'),
validators=[
WhitelistedMimeType({'application/pdf'}),
FileSizeLimit(100 * 1024 * 1024)
],
fieldset=_('Related link')
)
[docs]
color_hint = PanelField(
label=_('Color suggestions'),
fieldset=_('Colors'),
hide_label=False,
text=(
'AL #a74c97\n'
'BDP #a9cf00\n'
'Die Mitte #d28b00\n'
'EDU #7f6b65\n'
'EVP #e3c700\n'
'FDP #0084c7\n'
'GLP #aeca00\n'
'GRÜNE #54ba00\n'
'Piraten #333333\n'
'SP #c31906\n'
'SVP #408b3d\n'
),
kind='',
)
[docs]
def parse_colors(self, text: str | None) -> dict[str, str]:
if not text:
return {}
result = {
key.strip(): value
for key, value in findall(r'(.+)\s+(\#[0-9a-fA-F]{6})', text)
}
if len(text.strip().splitlines()) != len(result):
raise ValueError('Could not parse colors')
return result
[docs]
def validate_colors(self, field: TextAreaField) -> None:
try:
self.parse_colors(field.data)
except Exception as exception:
raise ValidationError(
_('Invalid color definitions')
) from exception
[docs]
def validate_id(self, field: StringField) -> None:
if normalize_for_url(field.data or '') != field.data:
raise ValidationError(_('Invalid ID'))
if self.model.id != field.data:
query = self.request.session.query(Election)
query = query.filter_by(id=field.data)
if query.first():
raise ValidationError(_('ID already exists'))
[docs]
def validate_external_id(self, field: StringField) -> None:
if field.data:
if (
not hasattr(self.model, 'external_id')
or self.model.external_id != field.data
):
for cls in (Election, ElectionCompound):
query = self.request.session.query(cls)
query = query.filter_by(external_id=field.data)
if query.first():
raise ValidationError(_('ID already exists'))
[docs]
def on_request(self) -> None:
principal = self.request.app.principal
self.domain.choices = [
(key, self.request.translate(text))
for key, text in principal.domains_election.items()
]
self.region.label.text = principal.label('region')
regions = {
region
for year in principal.entities.values()
for entity in year.values()
if (region := entity.get('region', None))
}
self.region.choices = [(item, item) for item in sorted(regions)]
self.district.label.text = principal.label('district')
districts = {
district
for year in principal.entities.values()
for entity in year.values()
if (district := entity.get('district', None))
}
self.district.choices = [(item, item) for item in sorted(districts)]
municipalities = {
municipality
for year in principal.entities.values()
for entity in year.values()
if (municipality := entity.get('name', None))
}
self.municipality.choices = [
(item, item) for item in sorted(municipalities)
]
if principal.domain == 'municipality':
assert principal.name is not None
self.municipality.choices = [(principal.name, principal.name)]
self.title_de.validators = []
self.title_fr.validators = []
self.title_it.validators = []
self.title_rm.validators = []
default_locale = self.request.default_locale or ''
if default_locale.startswith('de'):
self.title_de.validators.append(InputRequired())
if default_locale.startswith('fr'):
self.title_fr.validators.append(InputRequired())
if default_locale.startswith('it'):
self.title_it.validators.append(InputRequired())
if default_locale.startswith('rm'):
self.title_rm.validators.append(InputRequired())
layout = DefaultLayout(None, self.request)
query = self.request.session.query(Election)
query = query.order_by(Election.date.desc(), Election.shortcode)
choices: list[_Choice] = [
(
election.id,
'{} {} {}'.format(
layout.format_date(election.date, 'date'),
election.shortcode or '',
election.title,
).strip().replace(' ', ' ')
) for election in query
]
self.related_elections_historical.choices = choices
self.related_elections_other.choices = choices
[docs]
def update_realtionships(self, model: Election, type_: str) -> None:
# use symetric relationships
session = self.request.session
query = session.query(ElectionRelationship)
query = query.filter(
or_(
ElectionRelationship.source_id == model.id,
ElectionRelationship.target_id == model.id,
),
ElectionRelationship.type == type_
)
for relationship in query:
session.delete(relationship)
data = getattr(self, f'related_elections_{type_}', Bunch(data=[])).data
for id_ in data:
if not model.id:
model.id = model.id_from_title(session)
session.add(
ElectionRelationship(
source_id=model.id, target_id=id_, type=type_
)
)
session.add(
ElectionRelationship(
source_id=id_, target_id=model.id, type=type_
)
)
[docs]
def update_model(self, model: Election) -> None:
principal = self.request.app.principal
if self.id and self.id.data:
model.id = self.id.data
model.external_id = self.external_id.data
assert self.date.data is not None
model.date = self.date.data
model.domain = self.domain.data
model.domain_supersegment = ''
if model.domain == 'region':
model.domain_segment = self.region.data
model.domain_supersegment = principal.get_superregion(
self.region.data, self.date.data.year
)
if model.domain == 'district':
model.domain_segment = self.district.data
if model.domain == 'municipality':
model.domain_segment = self.municipality.data
model.type = self.type.data
model.shortcode = self.shortcode.data
assert self.mandates.data is not None
model.number_of_mandates = self.mandates.data
model.majority_type = self.majority_type.data
model.absolute_majority = self.absolute_majority.data or None
model.related_link = self.related_link.data
model.tacit = self.tacit.data
model.has_expats = self.has_expats.data
model.voters_counts = self.voters_counts.data
model.exact_voters_counts = self.exact_voters_counts.data
model.horizontal_party_strengths = self.horizontal_party_strengths.data
model.use_historical_party_results = (
self.use_historical_party_results.data)
model.show_party_strengths = self.show_party_strengths.data
model.show_party_panachage = self.show_party_panachage.data
titles = {}
if self.title_de.data:
titles['de_CH'] = self.title_de.data
if self.title_fr.data:
titles['fr_CH'] = self.title_fr.data
if self.title_it.data:
titles['it_CH'] = self.title_it.data
if self.title_rm.data:
titles['rm_CH'] = self.title_rm.data
model.title_translations = titles
titles = {}
if self.short_title_de.data:
titles['de_CH'] = self.short_title_de.data
if self.short_title_fr.data:
titles['fr_CH'] = self.short_title_fr.data
if self.short_title_it.data:
titles['it_CH'] = self.short_title_it.data
if self.short_title_rm.data:
titles['rm_CH'] = self.short_title_rm.data
model.short_title_translations = titles
link_labels = {}
if self.related_link_label_de.data:
link_labels['de_CH'] = self.related_link_label_de.data
if self.related_link_label_fr.data:
link_labels['fr_CH'] = self.related_link_label_fr.data
if self.related_link_label_it.data:
link_labels['it_CH'] = self.related_link_label_it.data
if self.related_link_label_rm.data:
link_labels['rm_CH'] = self.related_link_label_rm.data
model.related_link_label = link_labels
action = getattr(self.explanations_pdf, 'action', '')
if action == 'delete':
del model.explanations_pdf
if action == 'replace' and self.explanations_pdf.data:
assert self.explanations_pdf.file is not None
model.explanations_pdf = (
self.explanations_pdf.file,
self.explanations_pdf.filename,
)
model.colors = self.parse_colors(self.colors.data)
with self.request.session.no_autoflush:
self.update_realtionships(model, 'historical')
self.update_realtionships(model, 'other')
[docs]
def apply_model(self, model: Election) -> None:
self.id.data = model.id
self.external_id.data = model.external_id
titles = model.title_translations or {}
self.title_de.data = titles.get('de_CH')
self.title_fr.data = titles.get('fr_CH')
self.title_it.data = titles.get('it_CH')
self.title_rm.data = titles.get('rm_CH')
titles = model.short_title_translations or {}
self.short_title_de.data = titles.get('de_CH')
self.short_title_fr.data = titles.get('fr_CH')
self.short_title_it.data = titles.get('it_CH')
self.short_title_rm.data = titles.get('rm_CH')
link_labels = model.related_link_label or {}
self.related_link_label_de.data = link_labels.get('de_CH', '')
self.related_link_label_fr.data = link_labels.get('fr_CH', '')
self.related_link_label_it.data = link_labels.get('it_CH', '')
self.related_link_label_rm.data = link_labels.get('rm_CH', '')
file = model.explanations_pdf
if file:
self.explanations_pdf.data = {
'filename': file.reference.filename,
'size': file.reference.file.content_length,
'mimetype': file.reference.content_type
}
self.date.data = model.date
self.domain.data = model.domain
if model.domain == 'region':
self.region.data = model.domain_segment
if model.domain == 'district':
self.district.data = model.domain_segment
if model.domain == 'municipality':
self.municipality.data = model.domain_segment
self.shortcode.data = model.shortcode
self.type.data = model.type
self.mandates.data = model.number_of_mandates
self.majority_type.data = model.majority_type
self.absolute_majority.data = model.absolute_majority
self.related_link.data = model.related_link
self.tacit.data = model.tacit
self.has_expats.data = model.has_expats
self.horizontal_party_strengths.data = model.horizontal_party_strengths
self.use_historical_party_results.data = (
model.use_historical_party_results)
self.voters_counts.data = model.voters_counts
self.exact_voters_counts.data = model.exact_voters_counts
self.show_party_strengths.data = model.show_party_strengths
self.show_party_panachage.data = model.show_party_panachage
self.colors.data = '\n'.join(
f'{name} {model.colors[name]}' for name in sorted(model.colors)
)
if model.type == 'majorz':
self.type.choices = [
('majorz', _('Election based on the simple majority system'))
]
self.type.data = 'majorz'
else:
self.type.choices = [
('proporz', _('Election based on proportional representation'))
]
self.type.data = 'proporz'
self.related_elections_historical.choices = [
choice for choice in self.related_elections_historical.choices
if choice[0] != model.id
]
self.related_elections_other.choices = [
choice for choice in self.related_elections_other.choices
if choice[0] != model.id
]
self.related_elections_historical.data = [
association.target_id for association in model.related_elections
if association.type == 'historical'
]
self.related_elections_other.data = [
association.target_id for association in model.related_elections
if association.type == 'other'
]