Source code for election_day.forms.election_compound

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 ElectionCompoundRelationship
from onegov.form import Form
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 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 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 onegov.file import File
    from wtforms.fields.choices import _Choice


[docs] class ElectionCompoundForm(Form):
[docs] request: 'ElectionDayRequest'
[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] domain = RadioField( label=_('Domain'), fieldset=_('Properties'), choices=[ ('canton', _('Cantonal')) ], default='canton', validators=[ InputRequired() ] )
[docs] domain_elections = RadioField( label=_('Domain of the elections'), fieldset=_('Properties'), validators=[ InputRequired() ] )
[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] region_elections = ChosenSelectMultipleField( label=_('Elections'), fieldset=_('Properties'), choices=[], validators=[ InputRequired() ], depends_on=('domain_elections', 'region'), )
[docs] district_elections = ChosenSelectMultipleField( label=_('Elections'), fieldset=_('Properties'), choices=[], validators=[ InputRequired() ], depends_on=('domain_elections', 'district'), )
[docs] municipality_elections = ChosenSelectMultipleField( label=_('Elections'), fieldset=_('Properties'), choices=[], validators=[ InputRequired() ], depends_on=('domain_elections', 'municipality'), )
[docs] completes_manually = BooleanField( label=_('Completes manually'), description=_( 'Enables manual completion of the election compound. ' 'All elections are completed only once the election compound ' 'is completed.' ), fieldset=_('Completion'), render_kw={'force_simple': True} )
[docs] manually_completed = BooleanField( label=_('Completed'), fieldset=_('Completion'), depends_on=('completes_manually', 'y'), 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] related_compounds_historical = ChosenSelectMultipleField( label=_('Other legislative periods'), fieldset=_('Related elections'), choices=[] )
[docs] related_compounds_other = ChosenSelectMultipleField( label=_('Rounds of voting or by-elections'), fieldset=_('Related elections'), choices=[] )
[docs] explanations_pdf = UploadField( label=_('Explanations (PDF)'), validators=[ WhitelistedMimeType({'application/pdf'}), FileSizeLimit(100 * 1024 * 1024) ], fieldset=_('Related link') )
[docs] upper_apportionment_pdf = UploadField( label=_('Upper apportionment (PDF)'), validators=[ WhitelistedMimeType({'application/pdf'}), FileSizeLimit(100 * 1024 * 1024) ], fieldset=_('Related link'), depends_on=('pukelsheim', 'y'), )
[docs] lower_apportionment_pdf = UploadField( label=_('Lower apportionment (PDF)'), validators=[ WhitelistedMimeType({'application/pdf'}), FileSizeLimit(100 * 1024 * 1024) ], fieldset=_('Related link'), depends_on=('pukelsheim', 'y'), )
[docs] pukelsheim = BooleanField( label=_('Doppelter Pukelsheim'), fieldset=_('View options'), description=_('Allows to show the list groups and lists views.'), render_kw={'force_simple': True} )
[docs] voters_counts = BooleanField( label=_('Voters counts'), fieldset=_('View options'), description=_( 'Shows voters counts instead of votes in the party strengths ' 'view.' ), )
[docs] exact_voters_counts = BooleanField( label=_('Exact voters counts'), fieldset=_('View options'), description=_( 'Shows exact voters counts instead of rounded values.' ), 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=('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.' ), render_kw={'force_simple': True} )
[docs] show_seat_allocation = BooleanField( label=_('Seat allocation'), description=_( 'Shows a tab with the comparison of seat allocation as a bar ' 'chart. Requires party results.' ), fieldset=_('Views'), render_kw={'force_simple': True}, )
[docs] show_list_groups = BooleanField( label=_('List groups'), description=_( 'Shows a tab with list group results. Requires party results with ' 'voters counts. Only if Doppelter Pukelsheim.' ), fieldset=_('Views'), render_kw={'force_simple': True}, depends_on=('pukelsheim', 'y'), )
[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'), render_kw={'force_simple': True} )
[docs] show_party_panachage = BooleanField( label=_('Panachage'), description=_( 'Shows a tab with the panachage. Requires party results.' ), fieldset=_('Views'), render_kw={'force_simple': True} )
[docs] color_hint = PanelField( label=_('Color suggestions'), hide_label=False, fieldset=_('Colors'), 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] colors = TextAreaField( label=_('Colors'), fieldset=_('Colors'), render_kw={'rows': 12}, )
[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(ElectionCompound.id) 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_elections.choices = [] for domain in ('region', 'district', 'municipality'): if domain in principal.domains_election: self.domain_elections.choices.append(( domain, self.request.translate(principal.domains_election[domain]) )) 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_de.validators.append(InputRequired()) if default_locale.startswith('rm'): self.title_de.validators.append(InputRequired()) layout = DefaultLayout(None, self.request) query = self.request.session.query(Election) query = query.order_by(Election.date.desc(), Election.shortcode) query = query.filter(Election.type == 'proporz') self.region_elections.choices = [ ( item.id, '{} {} {}'.format( layout.format_date(item.date, 'date'), item.shortcode or '', item.title, ).replace(' ', ' ') ) for item in query.filter(Election.domain == 'region') ] self.district_elections.choices = [ ( item.id, '{} {} {}'.format( layout.format_date(item.date, 'date'), item.shortcode or '', item.title, ).replace(' ', ' ') ) for item in query.filter(Election.domain == 'district') ] self.municipality_elections.choices = [ ( item.id, '{} {} {}'.format( layout.format_date(item.date, 'date'), item.shortcode or '', item.title, ).replace(' ', ' ') ) for item in query.filter(Election.domain == 'municipality') ] query = self.request.session.query(ElectionCompound) query = query.order_by( ElectionCompound.date.desc(), ElectionCompound.shortcode ) choices: list[_Choice] = [ ( compound.id, '{} {} {}'.format( layout.format_date(compound.date, 'date'), compound.shortcode or '', compound.title, ).strip().replace(' ', ' ') ) for compound in query ] self.related_compounds_historical.choices = choices self.related_compounds_other.choices = choices
[docs] def update_realtionships( self, model: ElectionCompound, type_: str ) -> None: # use symetric relationships session = self.request.session query = session.query(ElectionCompoundRelationship) query = query.filter( or_( ElectionCompoundRelationship.source_id == model.id, ElectionCompoundRelationship.target_id == model.id, ), ElectionCompoundRelationship.type == type_ ) for relationship in query: session.delete(relationship) data = getattr(self, f'related_compounds_{type_}', Bunch(data=[])).data for id_ in data: if not model.id: model.id = model.id_from_title(session) session.add( ElectionCompoundRelationship( source_id=model.id, target_id=id_, type=type_ ) ) session.add( ElectionCompoundRelationship( source_id=id_, target_id=model.id, type=type_ ) )
[docs] def update_model(self, model: ElectionCompound) -> None: if self.id and self.id.data: model.id = self.id.data model.external_id = self.external_id.data model.domain = self.domain.data model.domain_elections = self.domain_elections.data assert self.date.data is not None model.date = self.date.data model.shortcode = self.shortcode.data model.related_link = self.related_link.data model.show_seat_allocation = self.show_seat_allocation.data model.show_list_groups = self.show_list_groups.data model.show_party_strengths = self.show_party_strengths.data model.show_party_panachage = self.show_party_panachage.data model.pukelsheim = self.pukelsheim.data model.completes_manually = self.completes_manually.data model.manually_completed = self.manually_completed.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.elections = [] query = self.request.session.query(Election) if self.domain_elections.data == 'region': if self.region_elections.data: model.elections = query.filter( Election.id.in_(self.region_elections.data) ).all() if self.domain_elections.data == 'district': if self.district_elections.data: model.elections = query.filter( Election.id.in_(self.district_elections.data) ).all() if self.domain_elections.data == 'municipality': if self.municipality_elections.data: model.elections = query.filter( Election.id.in_(self.municipality_elections.data) ).all() 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 for file in ( 'explanations_pdf', 'upper_apportionment_pdf', 'lower_apportionment_pdf' ): field = getattr(self, file) action = getattr(field, 'action', '') if action == 'delete': delattr(model, file) if action == 'replace' and field.data: setattr(model, file, (field.file, field.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: ElectionCompound) -> 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', '') for file_attr in ( 'explanations_pdf', 'upper_apportionment_pdf', 'lower_apportionment_pdf' ): field = getattr(self, file_attr) file: File = getattr(model, file_attr) if file: field.data = { 'filename': file.reference.filename, 'size': file.reference.file.content_length, 'mimetype': file.reference.content_type } self.domain.data = model.domain self.domain_elections.data = model.domain_elections self.date.data = model.date self.shortcode.data = model.shortcode self.related_link.data = model.related_link self.pukelsheim.data = model.pukelsheim self.completes_manually.data = model.completes_manually self.manually_completed.data = model.manually_completed self.voters_counts.data = model.voters_counts self.exact_voters_counts.data = model.exact_voters_counts self.horizontal_party_strengths.data = model.horizontal_party_strengths self.use_historical_party_results.data = ( model.use_historical_party_results) self.show_seat_allocation.data = model.show_seat_allocation self.show_list_groups.data = model.show_list_groups self.show_party_strengths.data = model.show_party_strengths self.show_party_panachage.data = model.show_party_panachage self.region_elections.data = [] if model.domain_elections == 'region': self.region_elections.data = [e.id for e in model.elections] self.district_elections.data = [] if model.domain_elections == 'district': self.district_elections.data = [e.id for e in model.elections] self.municipality_elections.data = [] if model.domain_elections == 'municipality': self.municipality_elections.data = [e.id for e in model.elections] self.colors.data = '\n'.join( f'{name} {model.colors[name]}' for name in sorted(model.colors) ) self.related_compounds_historical.choices = [ choice for choice in self.related_compounds_historical.choices if choice[0] != model.id ] self.related_compounds_other.choices = [ choice for choice in self.related_compounds_other.choices if choice[0] != model.id ] self.related_compounds_historical.data = [ association.target_id for association in model.related_compounds if association.type == 'historical' ] self.related_compounds_other.data = [ association.target_id for association in model.related_compounds if association.type == 'other' ]