diff --git a/src/onegov/pas/forms/attendence.py b/src/onegov/pas/forms/attendence.py index e73bbb4009..dca79733c2 100644 --- a/src/onegov/pas/forms/attendence.py +++ b/src/onegov/pas/forms/attendence.py @@ -210,8 +210,6 @@ class AttendenceAddCommissionForm(Form, SettlementRunBoundMixin): validators=[InputRequired()], ) - # todo: - def get_useful_data(self) -> dict[str, 'Any']: # type:ignore[override] result = super().get_useful_data() result['commission_id'] = self.model.id diff --git a/src/onegov/pas/forms/parliamentarian.py b/src/onegov/pas/forms/parliamentarian.py index e06c9e6ee5..d2e14c41a9 100644 --- a/src/onegov/pas/forms/parliamentarian.py +++ b/src/onegov/pas/forms/parliamentarian.py @@ -1,6 +1,8 @@ +from onegov.form.fields import PhoneNumberField from onegov.form.fields import TranslatedSelectField from onegov.form.fields import UploadField from onegov.form.forms import NamedFileForm +from onegov.form.validators import ValidPhoneNumber from onegov.pas import _ from onegov.pas.models.parliamentarian import GENDERS from onegov.pas.models.parliamentarian import SHIPPING_METHODS @@ -150,22 +152,25 @@ class ParliamentarianForm(NamedFileForm): fieldset=_('Additional information'), ) - # todo: phone number field and validator? - phone_private = StringField( + phone_private = PhoneNumberField( label=_('Private phone number'), fieldset=_('Additional information'), + validators=[ValidPhoneNumber()], + render_kw={'autocomplete': 'tel'} ) - # todo: phone number field and validator? - phone_mobile = StringField( + phone_mobile = PhoneNumberField( label=_('Mobile phone number'), fieldset=_('Additional information'), + validators=[ValidPhoneNumber()], + render_kw={'autocomplete': 'tel'} ) - # todo: phone number field and validator? - phone_business = StringField( + phone_business = PhoneNumberField( label=_('Business phone number'), fieldset=_('Additional information'), + validators=[ValidPhoneNumber()], + render_kw={'autocomplete': 'tel'} ) email_primary = EmailField( diff --git a/src/onegov/pas/forms/parliamentarian_role.py b/src/onegov/pas/forms/parliamentarian_role.py index 0ef4d4b98a..8811e7ca8c 100644 --- a/src/onegov/pas/forms/parliamentarian_role.py +++ b/src/onegov/pas/forms/parliamentarian_role.py @@ -62,7 +62,7 @@ class ParliamentarianRoleForm(Form): parliamentary_group_role = TranslatedSelectField( label=_('Parliamentary group role'), choices=list(PARLIAMENTARY_GROUP_ROLES.items()), - default='noe' + default='none' ) def on_request(self) -> None: diff --git a/src/onegov/pas/forms/rate_set.py b/src/onegov/pas/forms/rate_set.py index bd19538e32..30b2f4d7e2 100644 --- a/src/onegov/pas/forms/rate_set.py +++ b/src/onegov/pas/forms/rate_set.py @@ -225,9 +225,11 @@ class RateSetForm(Form): ) def validate_year(self, field: IntegerField) -> None: - if field.data is not None and not isinstance(self.model, RateSet): + if field.data is not None: query = self.request.session.query(RateSet) - query = query.filter_by(year=field.data) + query = query.filter(RateSet.year == field.data) + if isinstance(self.model, RateSet): + query = query.filter(RateSet.id != self.model.id) if query.first(): raise ValidationError(_( 'Rate set for ${year} alredy exists', diff --git a/src/onegov/pas/layouts/commission.py b/src/onegov/pas/layouts/commission.py index 3fd3a68e48..6c9d1629fd 100644 --- a/src/onegov/pas/layouts/commission.py +++ b/src/onegov/pas/layouts/commission.py @@ -22,6 +22,7 @@ def og_description(self) -> str: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link(self.title, self.request.link(self.model)) ] @@ -61,6 +62,7 @@ def og_description(self) -> str: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link( _('Commissions'), self.request.link(self.collection) diff --git a/src/onegov/pas/layouts/commission_membership.py b/src/onegov/pas/layouts/commission_membership.py index da3c165156..883c243ba2 100644 --- a/src/onegov/pas/layouts/commission_membership.py +++ b/src/onegov/pas/layouts/commission_membership.py @@ -25,6 +25,7 @@ def commission_collection(self) -> CommissionCollection: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link( _('Commissions'), self.request.link(self.commission_collection) diff --git a/src/onegov/pas/layouts/default.py b/src/onegov/pas/layouts/default.py index bde01a17cb..df2a9dc198 100644 --- a/src/onegov/pas/layouts/default.py +++ b/src/onegov/pas/layouts/default.py @@ -10,7 +10,7 @@ def pas_settings_url(self) -> str: return self.request.link(self.app.org, 'pas-settings') def format_minutes(self, value: int | None) -> str: - if not value: + if not value or value < 0: return '' hours = value // 60 diff --git a/src/onegov/pas/layouts/parliamentarian.py b/src/onegov/pas/layouts/parliamentarian.py index 7ab7dc8814..15e8d3b4d4 100644 --- a/src/onegov/pas/layouts/parliamentarian.py +++ b/src/onegov/pas/layouts/parliamentarian.py @@ -22,6 +22,7 @@ def og_description(self) -> str: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link(self.title, self.request.link(self.model)) ] @@ -61,6 +62,7 @@ def og_description(self) -> str: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link( _('Parliamentarians'), self.request.link(self.collection) diff --git a/src/onegov/pas/layouts/parliamentarian_role.py b/src/onegov/pas/layouts/parliamentarian_role.py index 7e92594dd3..6e48e3a19b 100644 --- a/src/onegov/pas/layouts/parliamentarian_role.py +++ b/src/onegov/pas/layouts/parliamentarian_role.py @@ -25,6 +25,7 @@ def parliamentarian_collection(self) -> ParliamentarianCollection: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link( _('Parliamentarians'), self.request.link(self.parliamentarian_collection) diff --git a/src/onegov/pas/layouts/settlement_run.py b/src/onegov/pas/layouts/settlement_run.py index 0fbf2c43bc..5b570180f2 100644 --- a/src/onegov/pas/layouts/settlement_run.py +++ b/src/onegov/pas/layouts/settlement_run.py @@ -22,6 +22,7 @@ def og_description(self) -> str: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link(self.title, self.request.link(self.model)) ] @@ -61,6 +62,7 @@ def og_description(self) -> str: def breadcrumbs(self) -> list[Link]: return [ Link(_('Homepage'), self.homepage_url), + Link(_('PAS settings'), self.pas_settings_url), Link( _('Settlement runs'), self.request.link(self.collection) diff --git a/src/onegov/pas/models/parliamentarian.py b/src/onegov/pas/models/parliamentarian.py index 0fdf4e3c3c..2123f60ac9 100644 --- a/src/onegov/pas/models/parliamentarian.py +++ b/src/onegov/pas/models/parliamentarian.py @@ -293,10 +293,8 @@ def shipping_method_label(self) -> str: @property def active(self) -> bool: - # todo: add hybrid property? if not self.roles: return True - for role in self.roles: if role.end is None or role.end >= date.today(): return True diff --git a/tests/onegov/pas/test_forms.py b/tests/onegov/pas/test_forms.py new file mode 100644 index 0000000000..62e78a942e --- /dev/null +++ b/tests/onegov/pas/test_forms.py @@ -0,0 +1,357 @@ +from datetime import date +from freezegun import freeze_time +from onegov.core.utils import Bunch +from onegov.pas.collections import CommissionCollection +from onegov.pas.collections import CommissionMembershipCollection +from onegov.pas.collections import ParliamentarianCollection +from onegov.pas.collections import ParliamentarianRoleCollection +from onegov.pas.collections import ParliamentaryGroupCollection +from onegov.pas.collections import PartyCollection +from onegov.pas.collections import RateSetCollection +from onegov.pas.collections import SettlementRunCollection +from onegov.pas.forms import AttendenceAddCommissionForm +from onegov.pas.forms import AttendenceAddForm +from onegov.pas.forms import AttendenceAddPlenaryForm +from onegov.pas.forms import AttendenceForm +from onegov.pas.forms import CommissionMembershipAddForm +from onegov.pas.forms import CommissionMembershipForm +from onegov.pas.forms import ParliamentarianRoleForm +from onegov.pas.forms import RateSetForm +from onegov.pas.forms import SettlementRunForm +from onegov.pas.models import RateSet +from onegov.pas.models import SettlementRun +from pytest import fixture + + +class DummyPostData(dict): + def getlist(self, key): + v = self[key] + if not isinstance(v, (list, tuple)): + v = [v] + return v + + +@fixture(scope='function') +def dummy_request(session): + return Bunch( + app=Bunch( + org=Bunch( + geo_provider=None, + open_files_target_blank=False + ), + schema='foo', + sentry_dsn=None, + version='1.0', + websockets_client_url=lambda x: x, + websockets_private_channel=None + ), + include=lambda x: x, + is_manager=True, + locale='de_CH', + session=session + ) + + +@freeze_time('2024-01-01') +def test_attendence_forms(session, dummy_request): + parliamentarians = ParliamentarianCollection(session) + parliamentarian = parliamentarians.add(first_name='a', last_name='b') + parliamentarians.add(first_name='p', last_name='q') + + roles = ParliamentarianRoleCollection(session) + roles.add(parliamentarian_id=parliamentarian.id, end=date(2023, 1, 1)) + + commissions = CommissionCollection(session) + commission = commissions.add(name='x') + commissions.add(name='y') + commissions.add(name='z') + + # on request + form = AttendenceForm() + form.request = dummy_request + form.on_request() + + assert len(form.parliamentarian_id.choices) == 2 + assert len(form.commission_id.choices) == 4 + + form = AttendenceAddForm() + form.request = dummy_request + form.on_request() + + assert len(form.parliamentarian_id.choices) == 1 + + # populate / get useful data + form = AttendenceForm(DummyPostData({ + 'commission_id': '', + 'duration': '2' + })) + + assert form.get_useful_data()['commission_id'] is None + assert form.get_useful_data()['duration'] == 120 + + obj = Bunch() + form.populate_obj(obj) + assert obj.commission_id is None + assert obj.duration == 120 + + form = AttendenceForm(DummyPostData({ + 'commission_id': commission.id, + 'duration': '2', + 'type': 'plenary' + })) + + assert form.get_useful_data()['commission_id'] is None + + obj = Bunch() + form.populate_obj(obj) + assert obj.commission_id is None + + # ensure commission + # todo: + + # ensure date + # todo: + + +@freeze_time('2024-01-01') +def test_add_plenary_attendence_form(session, dummy_request): + parliamentarians = ParliamentarianCollection(session) + parliamentarian = parliamentarians.add(first_name='a', last_name='b') + parliamentarians.add(first_name='p', last_name='q') + + roles = ParliamentarianRoleCollection(session) + roles.add(parliamentarian_id=parliamentarian.id, end=date(2023, 1, 1)) + + # on request + form = AttendenceAddPlenaryForm() + form.request = dummy_request + form.on_request() + + assert len(form.parliamentarian_id.choices) == 1 + assert len(form.parliamentarian_id.data) == 1 + + # get useful data + form = AttendenceAddPlenaryForm(DummyPostData({'duration': '2'})) + assert form.get_useful_data()['duration'] == 120 + + # ensure date + # todo: + + +@freeze_time('2024-01-01') +def test_add_commission_attendence_form(session, dummy_request): + parliamentarians = ParliamentarianCollection(session) + parliamentarian = parliamentarians.add(first_name='a', last_name='b') + parliamentarians.add(first_name='p', last_name='q') + + commissions = CommissionCollection(session) + commission = commissions.add(name='x') + + memberships = CommissionMembershipCollection(session) + memberships.add( + commission_id=commission.id, + parliamentarian_id=parliamentarian.id, + ) + + # on request + form = AttendenceAddCommissionForm() + form.request = dummy_request + form.model = commission + form.on_request() + + assert len(form.parliamentarian_id.choices) == 1 + assert len(form.parliamentarian_id.data) == 1 + + # get useful data + form = AttendenceAddCommissionForm(DummyPostData({'duration': '2'})) + form.model = commission + assert form.get_useful_data()['duration'] == 120 + assert form.get_useful_data()['commission_id'] == commission.id + + # ensure date + # todo: + + +@freeze_time('2024-01-01') +def test_commission_membership_forms(session, dummy_request): + parliamentarians = ParliamentarianCollection(session) + parliamentarian = parliamentarians.add(first_name='a', last_name='b') + parliamentarians.add(first_name='p', last_name='q') + + roles = ParliamentarianRoleCollection(session) + roles.add(parliamentarian_id=parliamentarian.id, end=date(2023, 1, 1)) + + commissions = CommissionCollection(session) + commissions.add(name='x') + commissions.add(name='y') + commissions.add(name='z') + + # on request + form = CommissionMembershipForm() + form.request = dummy_request + form.on_request() + + assert len(form.parliamentarian_id.choices) == 2 + assert len(form.commission_id.choices) == 3 + + form = CommissionMembershipAddForm() + form.request = dummy_request + form.on_request() + + assert len(form.parliamentarian_id.choices) == 1 + assert form.commission_id is None + + +def test_parliamentarian_role_form(session, dummy_request): + parliamentarians = ParliamentarianCollection(session) + parliamentarians.add(first_name='p', last_name='q') + + groups = ParliamentaryGroupCollection(session) + groups.add(name='a') + groups.add(name='b') + groups.add(name='c') + + parties = PartyCollection(session) + parties.add(name='x') + parties.add(name='y') + + # on request + form = ParliamentarianRoleForm() + form.request = dummy_request + form.on_request() + + assert len(form.parliamentarian_id.choices) == 1 + assert len(form.parliamentary_group_id.choices) == 4 + assert len(form.party_id.choices) == 3 + + # populate / get useful data + form = ParliamentarianRoleForm(DummyPostData({ + 'parliamentary_group_id': '', + 'party_id': '' + })) + + assert form.get_useful_data()['parliamentary_group_id'] is None + assert form.get_useful_data()['party_id'] is None + + obj = Bunch() + form.populate_obj(obj) + assert obj.parliamentary_group_id is None + assert obj.party_id is None + + +@freeze_time('2022-06-06') +def test_rate_set_form(session, dummy_request): + collection = RateSetCollection(session) + rate_set = collection.add(year=2020) + + # default year + form = RateSetForm(DummyPostData()) + assert form.data['year'] == 2022 + + # add + form = RateSetForm(DummyPostData({'year': 2022})) + form.model = collection + form.request = dummy_request + assert not form.validate() + assert 'year' not in form.errors + + form = RateSetForm(DummyPostData({'year': 2020})) + form.model = collection + form.request = dummy_request + assert not form.validate() + assert form.errors['year'][0].interpolate() == \ + 'Rate set for 2020 alredy exists' + + # edit + form = RateSetForm(DummyPostData({'year': 2022})) + form.model = RateSet(year=2021) + form.request = dummy_request + assert not form.validate() + assert 'year' not in form.errors + + form = RateSetForm(DummyPostData({'year': 2020})) + form.model = RateSet(year=2021) + form.request = dummy_request + assert not form.validate() + assert form.errors['year'][0].interpolate() == \ + 'Rate set for 2020 alredy exists' + + form = RateSetForm(DummyPostData({'year': 2020})) + form.model = rate_set + form.request = dummy_request + assert not form.validate() + assert 'year' not in form.errors + + +def test_settlement_run_form(session, dummy_request): + collection = SettlementRunCollection(session) + settlement_run = collection.add( + name='2022', + start=date(2022, 1, 1), + end=date(2022, 12, 31), + active=True + ) + + # range + form = SettlementRunForm(DummyPostData({ + 'start': '2021-01-01', + 'end': '2020-01-01', + })) + form.request = dummy_request + assert not form.validate() + assert form.errors['end'][0] == 'End must be after start' + + # add + for start, end, overlaps in ( + ('2021-11-11', '2021-12-12', False), + ('2021-12-12', '2022-02-02', True), + ('2022-01-01', '2022-02-02', True), + ('2022-02-02', '2022-04-04', True), + ('2022-11-11', '2022-12-31', True), + ('2023-01-01', '2023-02-02', False), + ): + form = SettlementRunForm(DummyPostData({'start': start, 'end': end})) + form.model = collection + form.request = dummy_request + assert not form.validate() + if overlaps: + message = ( + 'Dates overlap with existing settlement run: ' + '01.01.2022 - 31.12.2022' + ) + assert form.errors['start'][0].interpolate() == message + assert form.errors['end'][0].interpolate() == message + else: + assert 'start' not in form.errors + assert 'end' not in form.errors + + # edit + form = SettlementRunForm(DummyPostData({ + 'start': '2020-02-02', + 'end': '2020-03-03' + })) + form.model = SettlementRun() + form.request = dummy_request + assert not form.validate() + assert 'start' not in form.errors + assert 'end' not in form.errors + + form = SettlementRunForm(DummyPostData({ + 'start': '2022-02-02', + 'end': '2022-03-03' + })) + form.model = SettlementRun() + form.request = dummy_request + assert not form.validate() + assert form.errors['start'][0].startswith('Dates overlap') + assert form.errors['end'][0].startswith('Dates overlap') + + form = SettlementRunForm(DummyPostData({ + 'start': '2022-02-02', + 'end': '2022-03-03' + })) + form.model = settlement_run + form.request = dummy_request + assert not form.validate() + assert 'start' not in form.errors + assert 'end' not in form.errors diff --git a/tests/onegov/pas/test_layouts.py b/tests/onegov/pas/test_layouts.py new file mode 100644 index 0000000000..324774bc96 --- /dev/null +++ b/tests/onegov/pas/test_layouts.py @@ -0,0 +1,28 @@ +from onegov.pas.layouts import DefaultLayout +from onegov.core.utils import Bunch + + +def test_layouts(): + request = Bunch( + app=Bunch( + org=Bunch( + geo_provider=None, + open_files_target_blank=False + ), + schema='foo', + sentry_dsn=None, + version='1.0', + websockets_client_url=lambda x: x, + websockets_private_channel=None + ), + include=lambda x: x, + is_manager=True + ) + + layout = DefaultLayout(None, request) + assert layout.format_minutes(None) == '' + assert layout.format_minutes(0) == '' + assert layout.format_minutes(-20) == '' + assert layout.format_minutes(10).interpolate() == '10 minutes' + assert layout.format_minutes(60).interpolate() == '1 hours' + assert layout.format_minutes(123).interpolate() == '2 hours 3 minutes' diff --git a/tests/onegov/pas/test_models.py b/tests/onegov/pas/test_models.py new file mode 100644 index 0000000000..6bfb49a651 --- /dev/null +++ b/tests/onegov/pas/test_models.py @@ -0,0 +1,129 @@ +from freezegun import freeze_time +from datetime import date +from onegov.core.utils import Bunch +from onegov.pas.models import Attendence +from onegov.pas.models import Change +from onegov.pas.models import Commission +from onegov.pas.models import CommissionMembership +from onegov.pas.models import LegislativePeriod +from onegov.pas.models import Parliamentarian +from onegov.pas.models import ParliamentarianRole +from onegov.pas.models import ParliamentaryGroup +from onegov.pas.models import Party +from onegov.pas.models import RateSet +from onegov.pas.models import SettlementRun + + +@freeze_time('2022-06-06') +def test_models(session): + rate_set = RateSet(year=2022) + legislative_period = LegislativePeriod( + name='2022-2024', + start=date(2022, 1, 1), + end=date(2022, 12, 31) + ) + settlement_run = SettlementRun( + name='2022-1', + start=date(2022, 1, 1), + end=date(2022, 3, 31), + active=True + ) + parliamentary_group = ParliamentaryGroup(name='Group') + party = Party(name='Party') + commission = Commission( + name='Official Commission', + type='official' + ) + parliamentarian = Parliamentarian( + first_name='First', + last_name='Last', + gender='female', + shipping_method='plus' + ) + session.add(rate_set) + session.add(legislative_period) + session.add(settlement_run) + session.add(parliamentary_group) + session.add(party) + session.add(commission) + session.add(parliamentarian) + session.flush() + + parliamentarian_role = ParliamentarianRole( + parliamentarian_id=parliamentarian.id, + role='vice_president', + party_id=party.id, + party_role='media_manager', + parliamentary_group_id=parliamentary_group.id, + parliamentary_group_role='vote_counter' + ) + commission_membership = CommissionMembership( + role='president', + commission_id=commission.id, + parliamentarian_id=parliamentarian.id + ) + attendence = Attendence( + date=date(2022, 1, 1), + duration=30, + type='commission', + parliamentarian_id=parliamentarian.id, + commission_id=commission.id + ) + session.add(parliamentarian_role) + session.add(commission_membership) + session.add(attendence) + session.flush() + + change = Change.add( + request=Bunch( + current_username='user@example.org', + current_user=Bunch(title='User'), + session=session + ), + action='add', + attendence=attendence + ) + session.add(change) + session.flush() + + # labels + assert attendence.type_label == 'Commission meeting' + assert commission.type_label == 'official mission' + assert commission_membership.role_label == 'President' + assert parliamentarian.gender_label == 'female' + assert parliamentarian.shipping_method_label == 'A mail plus' + assert parliamentarian_role.role_label == 'Vice president' + assert parliamentarian_role.party_role_label == 'Media Manager' + assert parliamentarian_role.parliamentary_group_role_label == \ + 'Vote counter' + assert change.action_label == 'Attendence added' + + # relationships + assert commission.memberships == [commission_membership] + assert commission.attendences == [attendence] + assert parliamentarian.roles == [parliamentarian_role] + assert parliamentarian.commission_memberships == [commission_membership] + assert parliamentarian.attendences == [attendence] + assert parliamentary_group.roles == [parliamentarian_role] + assert party.roles == [parliamentarian_role] + assert change.attendence == attendence + assert change.parliamentarian == parliamentarian + assert change.commission == commission + + # properties + assert parliamentarian.title == 'First Last' + assert change.user == 'User (user@example.org)' + assert change.date == date(2022, 1, 1) + + # ... parliamentarian.active + assert parliamentarian.active is True + parliamentarian_role.end = date(2022, 5, 5) + assert parliamentarian.active is False + parliamentarian.roles = [] + assert parliamentarian.active is True + + # commission.end_observer + assert parliamentarian.active is True + commission.end = date(2022, 5, 5) + session.flush() + assert commission_membership.end == date(2022, 5, 5)