from __future__ import annotations
import datetime
from collections.abc import Mapping
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 operator import itemgetter
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import object_session
from sqlalchemy.orm import relationship
from sqlalchemy.orm import DynamicMapped
from sqlalchemy.orm import Mapped
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Collection
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
from sqlalchemy.orm import AppenderQuery
[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: Mapped[str] = mapped_column(primary_key=True)
#: external identifier
[docs]
external_id: Mapped[str | None]
#: all translations of the title
[docs]
title_translations: Mapped[Mapping[str, str]] = mapped_column(HSTORE)
#: 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: Mapped[Mapping[str, str] | None] = mapped_column(
HSTORE
)
#: 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:
session = object_session(self)
assert session is not None
self.id = self.id_from_title(session)
#: Shortcode for cantons that use it
[docs]
shortcode: Mapped[str | None]
#: The date of the elections
[docs]
date: Mapped[datetime.date]
#: Doppelter Pukelsheim
[docs]
pukelsheim: Mapped[bool] = mapped_column(default=False)
#: Allow setting the status of the compound and its elections manually
[docs]
completes_manually: Mapped[bool] = mapped_column(default=False)
#: Status of the compound and its elections
[docs]
manually_completed: Mapped[bool] = mapped_column(default=False)
#: An election compound may contains n party results
[docs]
party_results: Mapped[list[PartyResult]] = relationship(
cascade='all, delete-orphan',
back_populates='election_compound',
overlaps='party_results'
)
#: An election compound may contains n party panachage results
[docs]
party_panachage_results: Mapped[list[PartyPanachageResult]] = relationship(
cascade='all, delete-orphan',
back_populates='election_compound',
overlaps='panachage_results'
)
#: An election compound may have related election compounds
#: An election compound may be related by other election compounds
[docs]
referencing_compounds: DynamicMapped[ElectionCompoundRelationship] = (
relationship(
foreign_keys='ElectionCompoundRelationship.target_id',
cascade='all, delete-orphan',
back_populates='target',
)
)
#: 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: Mapped[list[Election]] = relationship(
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, itemgetter(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: DynamicMapped[Notification] = relationship(
'onegov.election_day.models.notification.Notification',
back_populates='election_compound',
)
#: screens linked to this election compound
[docs]
screens: DynamicMapped[Screen] = relationship(
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)