from onegov.core.orm import Base, observes
from onegov.core.orm import translation_hybrid
from onegov.core.orm.mixins import ContentMixin
from onegov.core.orm.mixins import dict_property
from onegov.core.orm.mixins import meta_property
from onegov.core.orm.types import HSTORE
from onegov.core.utils import groupbylist
from onegov.election_day.models.election_compound.mixins import (
DerivedAttributesMixin)
from onegov.election_day.models.mixins import DomainOfInfluenceMixin
from onegov.election_day.models.mixins import ExplanationsPdfMixin
from onegov.election_day.models.mixins import IdFromTitlesMixin
from onegov.election_day.models.mixins import LastModifiedMixin
from onegov.election_day.models.mixins import TitleTranslationsMixin
from onegov.election_day.models.party_result.mixins import (
HistoricalPartyResultsMixin)
from onegov.election_day.models.party_result.mixins import (
PartyResultsCheckMixin)
from onegov.election_day.models.party_result.mixins import (
PartyResultsOptionsMixin)
from onegov.file import NamedFile
from sqlalchemy import Column, Boolean
from sqlalchemy import Date
from sqlalchemy import Text
from sqlalchemy.orm import object_session
from sqlalchemy.orm import relationship
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Collection
from collections.abc import Mapping
from onegov.core.types import AppenderQuery
from onegov.election_day.models import Election
from onegov.election_day.models import ElectionCompoundRelationship
from onegov.election_day.models import Notification
from onegov.election_day.models import PartyPanachageResult
from onegov.election_day.models import PartyResult
from onegov.election_day.models import Screen
from onegov.election_day.types import DomainOfInfluence
import datetime
[docs]
class ElectionCompound(
Base, ContentMixin, LastModifiedMixin,
DomainOfInfluenceMixin, TitleTranslationsMixin, IdFromTitlesMixin,
PartyResultsOptionsMixin, PartyResultsCheckMixin,
HistoricalPartyResultsMixin,
ExplanationsPdfMixin, DerivedAttributesMixin
):
[docs]
__tablename__ = 'election_compounds'
@property
[docs]
def polymorphic_base(self) -> type['ElectionCompound']:
return ElectionCompound
#: Identifies the election compound, may be used in the url
[docs]
id: 'Column[str]' = Column(Text, primary_key=True)
#: external identifier
[docs]
external_id: 'Column[str | None]' = Column(Text, nullable=True)
#: all translations of the title
[docs]
title_translations: 'Column[Mapping[str, str]]' = Column(
HSTORE,
nullable=False
)
#: the translated title (uses the locale of the request, falls back to the
#: default locale of the app)
[docs]
title = translation_hybrid(title_translations)
#: all translations of the short title
[docs]
short_title_translations: 'Column[Mapping[str, str] | None]' = Column(
HSTORE,
nullable=True
)
#: the translated short title (uses the locale of the request, falls back
#: to the default locale of the app)
[docs]
short_title = translation_hybrid(short_title_translations)
@observes('title_translations', 'short_title_translations')
[docs]
def title_observer(
self,
title_translations: 'Mapping[str, str]',
short_title_translations: 'Mapping[str, str]'
) -> None:
if not self.id:
self.id = self.id_from_title(object_session(self))
#: Shortcode for cantons that use it
[docs]
shortcode: 'Column[str | None]' = Column(Text, nullable=True)
#: The date of the elections
[docs]
date: 'Column[datetime.date]' = Column(Date, nullable=False)
#: Doppelter Pukelsheim
[docs]
pukelsheim: 'Column[bool]' = Column(Boolean, nullable=False, default=False)
#: Allow setting the status of the compound and its elections manually
[docs]
completes_manually: 'Column[bool]' = Column(
Boolean,
nullable=False,
default=False
)
#: Status of the compound and its elections
[docs]
manually_completed: 'Column[bool]' = Column(
Boolean,
nullable=False,
default=False
)
#: An election compound may contains n party results
[docs]
party_results: 'relationship[list[PartyResult]]' = relationship(
'PartyResult',
cascade='all, delete-orphan',
back_populates='election_compound'
)
#: An election compound may contains n party panachage results
[docs]
party_panachage_results: 'rel[list[PartyPanachageResult]]'
party_panachage_results = relationship(
'PartyPanachageResult',
cascade='all, delete-orphan',
back_populates='election_compound'
)
#: An election compound may have related election compounds
related_compounds = relationship(
'ElectionCompoundRelationship',
foreign_keys='ElectionCompoundRelationship.source_id',
cascade='all, delete-orphan',
back_populates='source',
lazy='dynamic'
)
#: An election compound may be related by other election compounds
[docs]
referencing_compounds: 'rel[AppenderQuery[ElectionCompoundRelationship]]'
referencing_compounds = relationship(
'ElectionCompoundRelationship',
foreign_keys='ElectionCompoundRelationship.target_id',
cascade='all, delete-orphan',
back_populates='target',
lazy='dynamic'
)
#: Defines optional colors for parties
[docs]
colors: dict_property[dict[str, str]] = meta_property(
'colors',
default=dict
)
#: Defines the domain of the elections
[docs]
domain_elections: dict_property['DomainOfInfluence'] = meta_property(
'domain_elections',
default='district'
)
#: An election compound may contain n elections
[docs]
elections: 'relationship[list[Election]]' = relationship(
'Election',
cascade='all',
back_populates='election_compound',
order_by='Election.shortcode'
)
@observes('elections')
[docs]
def elections_observer(
self,
elections: 'Collection[Election]'
) -> None:
changes = {c for e in elections if (c := e.last_result_change)}
if changes:
new = max(changes)
old = self.last_result_change
if not old or (old and old < new):
self.last_result_change = new
@property
[docs]
def progress(self) -> tuple[int, int]:
""" Returns a tuple with the current progress.
If the elections define a `domain_supersegment` (i.e. superregions),
this is the number of fully counted supersegments vs. the total number
of supersegments.
If no `domain_supersegment` is defined, this is the number of counted
elections vs. the total number of elections.
"""
pairs = sorted(
(e.domain_supersegment, e.completed)
for e in self.elections
)
grouped = groupbylist(pairs, lambda x: x[0])
if len(grouped) == 1 and grouped[0][0] == '':
result = [completed for _, completed in grouped[0][1]]
else:
result = [all(c for _, c in segment) for _, segment in grouped]
return sum(1 for r in result if r), len(result)
@property
[docs]
def has_results(self) -> bool:
""" Returns True, if the election compound has any results. """
if self.has_party_results:
return True
if self.has_party_panachage_results:
return True
for election in self.elections:
if election.has_results:
return True
return False
@property
[docs]
def elected_candidates(self) -> list[tuple[str, str]]:
""" Returns the first and last names of the elected candidates. """
result = []
for election in self.elections:
result.extend(election.elected_candidates)
return result
#: notifcations linked to this election compound
[docs]
notifications: 'relationship[AppenderQuery[Notification]]'
notifications = relationship( # type:ignore[misc]
'onegov.election_day.models.notification.Notification',
back_populates='election_compound',
lazy='dynamic'
)
#: screens linked to this election compound
[docs]
screens: 'relationship[AppenderQuery[Screen]]' = relationship(
'Screen',
back_populates='election_compound',
)
#: may be used to store a link related to this election
#: additional file in case of Doppelter Pukelsheim
[docs]
upper_apportionment_pdf = NamedFile()
#: additional file in case of Doppelter Pukelsheim
[docs]
lower_apportionment_pdf = NamedFile()
@property
[docs]
def relationships_for_historical_party_results(
self
) -> 'AppenderQuery[ElectionCompoundRelationship]':
return self.related_compounds
[docs]
def clear_results(self, clear_all: bool = False) -> None:
""" Clears all related results. """
self.last_result_change = None
self.party_results = []
self.party_panachage_results = []
for election in self.elections:
election.clear_results(clear_all)