Skip to content

RIS light initial commit #1843

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repos:
exclude: .pre-commit-config.yaml
- id: pt_structure
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
rev: v0.11.13
hooks:
- id: ruff
args: [ "--fix" ]
Expand Down Expand Up @@ -70,6 +70,6 @@ repos:
files: '^stubs/.*\.pyi$'
pass_filenames: false
- repo: https://github.com/gitleaks/gitleaks
rev: v8.27.0
rev: v8.27.2
hooks:
- id: gitleaks
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ packages =
onegov.onboarding
onegov.org
onegov.page
onegov.parliament
onegov.pas
onegov.pay
onegov.pdf
Expand Down Expand Up @@ -328,6 +329,7 @@ onegov_upgrades =
onegov.onboarding = onegov.onboarding.upgrade
onegov.org = onegov.org.upgrade
onegov.page = onegov.page.upgrade
onegov.parliament = onegov.parliament.upgrade
onegov.pas = onegov.pas.upgrade
onegov.pay = onegov.pay.upgrade
onegov.pdf = onegov.pdf.upgrade
Expand Down
1 change: 1 addition & 0 deletions src/onegov/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
'onegov.newsletter',
'onegov.notice',
'onegov.page',
'onegov.parliament',
'onegov.pay',
'onegov.pdf',
'onegov.people',
Expand Down
13 changes: 13 additions & 0 deletions src/onegov/parliament/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

import logging

log = logging.getLogger('onegov.parliament')
log.addHandler(logging.NullHandler())

from onegov.parliament.i18n import _

__all__ = (
'_',
'log'
)
5 changes: 5 additions & 0 deletions src/onegov/parliament/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from __future__ import annotations

from onegov.core.i18n.translation_string import TranslationStringFactory

_ = TranslationStringFactory('onegov.parliament')
36 changes: 36 additions & 0 deletions src/onegov/parliament/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

from onegov.parliament.models.attendence import Attendence
from onegov.parliament.models.change import Change
from onegov.parliament.models.commission import Commission
from onegov.parliament.models.commission_membership import CommissionMembership
from onegov.parliament.models.legislative_period import LegislativePeriod
from onegov.parliament.models.meeting import Meeting
from onegov.parliament.models.parliamentarian import (
Parliamentarian,
RISParliamentarian
)
from onegov.parliament.models.parliamentarian_role import ParliamentarianRole
from onegov.parliament.models.parliamentary_group import ParliamentaryGroup
from onegov.parliament.models.party import Party
from onegov.parliament.models.political_business import (
PoliticalBusiness,
PoliticalBusinessParticipation,
)


__all__ = (
'Attendence',
'Change',
'Commission',
'CommissionMembership',
'LegislativePeriod',
'Meeting',
'Parliamentarian',
'RISParliamentarian',
'ParliamentarianRole',
'ParliamentaryGroup',
'Party',
'PoliticalBusiness',
'PoliticalBusinessParticipation',
)
181 changes: 181 additions & 0 deletions src/onegov/parliament/models/attendence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
from __future__ import annotations

from decimal import ROUND_HALF_UP, Decimal

from sqlalchemy import Column
from sqlalchemy import Date
from sqlalchemy import Enum
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import Text
from sqlalchemy.orm import relationship
from uuid import uuid4

from onegov.core.orm import Base
from onegov.core.orm.mixins import TimestampMixin
from onegov.core.orm.types import UUID
from onegov.parliament import _

from typing import TYPE_CHECKING

if TYPE_CHECKING:
import uuid
import datetime
from typing import Literal
from typing import TypeAlias

from onegov.parliament.models import Parliamentarian, Commission

AttendenceType: TypeAlias = Literal[
'plenary',
'commission',
'study',
'shortest',
]

TYPES: dict[AttendenceType, str] = {
'plenary': _('Plenary session'),
'commission': _('Commission meeting'),
'study': _('File study'),
'shortest': _('Shortest meeting'),
}


class Attendence(Base, TimestampMixin):

__tablename__ = 'par_attendence'

poly_type: Column[str] = Column(
Text,
nullable=False,
default=lambda: 'generic'
)

__mapper_args__ = {
'polymorphic_on': poly_type,
'polymorphic_identity': 'generic',
}

#: Internal ID
id: Column[uuid.UUID] = Column(
UUID, # type:ignore[arg-type]
primary_key=True,
default=uuid4
)

#: The date
date: Column[datetime.date] = Column(
Date,
nullable=False
)

#: The duration in minutes
duration: Column[int] = Column(
Integer,
nullable=False
)

#: The type
type: Column[AttendenceType] = Column(
Enum(
*TYPES.keys(), # type:ignore[arg-type]
name='par_attendence_type'
),
nullable=False,
default='plenary'
)

#: The type as translated text
@property
def type_label(self) -> str:
return TYPES.get(self.type, '')

#: The id of the parliamentarian
parliamentarian_id: Column[uuid.UUID] = Column(
UUID, # type:ignore[arg-type]
ForeignKey('par_parliamentarians.id'),
nullable=False
)

#: The parliamentarian
parliamentarian: relationship[Parliamentarian] = relationship(
'Parliamentarian',
back_populates='attendences'
)

#: the id of the commission
commission_id: Column[uuid.UUID | None] = Column(
UUID, # type:ignore[arg-type]
ForeignKey('par_commissions.id'),
nullable=True
)

#: the related commission (which may have any number of memberships)
commission: relationship[Commission | None] = relationship(
'Commission',
back_populates='attendences'
)

def calculate_value(self) -> Decimal:
"""Calculate the value (in hours) for an attendance record.

The calculation follows these business rules:
- Plenary sessions:
* Always counted as 0.5 (half day), regardless of actual duration
This is the special case!

- Everything else is counted as actual hours:
* First 2 hours are counted as given
* After 2 hours, time is rounded to nearest 30-minute increment,
* and there is another rate applied for the additional time
* Example: 2h 40min would be calculated as 2.5 hours

Examples:
>>> # Plenary session
>>> attendence.type = 'plenary'
>>> calculate_value(attendence)
'0.5'

>>> # Commission meeting, 2 hours
>>> attendence.type = 'commission'
>>> attendence.duration = 120 # minutes
>>> calculate_value(attendence)
'2.0'

>>> # Study session, 2h 40min
>>> attendence.type = 'study'
>>> attendence.duration = 160 # minutes
>>> calculate_value(attendence)
'2.5'
"""
if self.duration < 0:
raise ValueError('Duration cannot be negative')

Check warning on line 152 in src/onegov/parliament/models/attendence.py

View check run for this annotation

Codecov / codecov/patch

src/onegov/parliament/models/attendence.py#L152

Added line #L152 was not covered by tests

if self.type == 'plenary':
return Decimal('0.5')

if self.type in ('commission', 'study', 'shortest'):
# Convert minutes to hours with Decimal for precise calculation
duration_hours = Decimal(str(self.duration)) / Decimal('60')

if duration_hours <= Decimal('2'):
# Round to 1 decimal place
return duration_hours.quantize(
Decimal('0.1'), rounding=ROUND_HALF_UP
)
else:
base_hours = Decimal('2')
additional_hours = (duration_hours - base_hours)
# Round additional time to nearest 0.5
additional_hours = (additional_hours * 2).quantize(
Decimal('1.0'), rounding=ROUND_HALF_UP
) / 2
total_hours = base_hours + additional_hours
return total_hours.quantize(
Decimal('0.1'), rounding=ROUND_HALF_UP
)

raise ValueError(f'Unknown attendance type: {self.type}')

Check warning on line 178 in src/onegov/parliament/models/attendence.py

View check run for this annotation

Codecov / codecov/patch

src/onegov/parliament/models/attendence.py#L178

Added line #L178 was not covered by tests

def __repr__(self) -> str:
return f'<Attendence {self.date} {self.type}>'

Check warning on line 181 in src/onegov/parliament/models/attendence.py

View check run for this annotation

Codecov / codecov/patch

src/onegov/parliament/models/attendence.py#L181

Added line #L181 was not covered by tests
Loading
Loading