Source code for pas.views.attendence

from collections import defaultdict
import uuid
from datetime import date, datetime

from sqlalchemy import false

from onegov.core.elements import BackLink, Confirm, Intercooler, Link
from onegov.core.security import Private
from onegov.core.templates import render_template
from onegov.core.utils import module_path
from onegov.core.utils import normalize_for_filename
from onegov.pas import _
from onegov.pas import PasApp
from onegov.pas.collections import (
    AttendenceCollection,
    PASParliamentarianCollection,
    PresidentialAllowanceCollection,
    SettlementRunCollection,
)
from onegov.pas.custom import (
    has_user_set_abschluss_for_commission,
    validate_attendance_date,
    notify_admins_finalized,
)
from onegov.pas.forms import AttendenceAddCommissionBulkForm, AttendenceAddForm
from onegov.pas.forms import AttendenceAddPlenaryForm
from onegov.pas.forms import AttendenceForm
from onegov.pas.forms.attendence import AttendenceAddFileStudyBulkForm
from onegov.pas.forms.attendence import AttendenceCommissionBulkEditForm
from onegov.pas.forms.attendence import AttendencePlenaryBulkEditForm
from onegov.pas.layouts import AttendenceCollectionLayout
from onegov.pas.layouts import AttendenceLayout
from onegov.pas.models import Attendence
from onegov.pas.models import Change
from onegov.pas.models import PresidentialAllowance
from onegov.pas.models import SettlementRun
from onegov.pas.models.attendence import TYPES
from onegov.pas.models.commission_membership import PASCommissionMembership
from onegov.pas.utils import (
    get_attendance_parliamentarians,
)
from onegov.user import User
from weasyprint import CSS, HTML  # type: ignore[import-untyped]
from uuid import UUID
from webob import Response
from webob.exc import HTTPNotFound
from weasyprint.text.fonts import (  # type: ignore[import-untyped]
    FontConfiguration,
)


from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from onegov.core.types import RenderData
    from onegov.town6.request import TownRequest
    from onegov.pas.request import PasRequest


@PasApp.html(
    model=AttendenceCollection,
    template='attendences.pt',
    permission=Private
)
[docs] def view_attendences( self: AttendenceCollection, request: PasRequest ) -> RenderData: layout = AttendenceCollectionLayout(self, request) attendences = self.query_for_current_user(request) groups: dict[str, list[Attendence]] = defaultdict(list) for a in attendences: if a.bulk_edit_id: groups[str(a.bulk_edit_id)].append(a) bulk_edit_groups = sorted( groups.values(), key=lambda g: max(a.modified or a.created for a in g), reverse=True, ) attendences_sorted = sorted( attendences, key=lambda a: a.created or a.modified or datetime.min, reverse=True, ) attendence_ids = {str(a.id) for a in attendences_sorted} creators: dict[str, str] = {} if attendence_ids: for change in request.session.query(Change).filter_by( model='attendence', action='add' ): change_data = change.changes or {} attendence_id = change_data.get('id') if ( isinstance(attendence_id, str) and attendence_id in attendence_ids and change.user ): creators.setdefault(attendence_id, change.user) type_filters = [ Link( text=request.translate(_('All')), active=self.type is None, url=request.link( self.for_filter( settlement_run_id=self.settlement_run_id, date_from=self.date_from, date_to=self.date_to, type=None, parliamentarian_id=self.parliamentarian_id, commission_id=self.commission_id, party_id=self.party_id, plenary_date=self.plenary_date, ) ), ) ] for key, label in TYPES.items(): if not request.is_admin and key == 'plenary': continue type_filters.append( Link( text=request.translate(label), active=self.type == key, url=request.link( self.for_filter( settlement_run_id=self.settlement_run_id, date_from=self.date_from, date_to=self.date_to, type=key, parliamentarian_id=self.parliamentarian_id, commission_id=self.commission_id, party_id=self.party_id, plenary_date=self.plenary_date, ) ), ) ) settlement_runs = ( SettlementRunCollection(request.session).query().all() ) run_filters = [ Link( text=request.translate(_('All')), active=self.settlement_run_id is None, url=request.link( self.for_filter( settlement_run_id='all', date_from=self.date_from, date_to=self.date_to, type=self.type, parliamentarian_id=self.parliamentarian_id, commission_id=self.commission_id, party_id=self.party_id, plenary_date=self.plenary_date, ) ), ) ] for run in settlement_runs: run_id = str(run.id) run_filters.append( Link( text=run.name, active=self.settlement_run_id == run_id, url=request.link( self.for_filter( settlement_run_id=run_id, date_from=self.date_from, date_to=self.date_to, type=self.type, parliamentarian_id=self.parliamentarian_id, commission_id=self.commission_id, party_id=self.party_id, plenary_date=self.plenary_date, ) ), ) ) plenary_filters: list[Link] = [] if request.is_admin: plenary_dates_query = ( request.session.query(Attendence.date) .filter(Attendence.type == 'plenary') .order_by(Attendence.date.desc()) .distinct() ) plenary_filters = [ Link( text=request.translate(_('All')), active=self.plenary_date is None, url=request.link( self.for_filter( settlement_run_id=self.settlement_run_id, date_from=self.date_from, date_to=self.date_to, type=self.type, parliamentarian_id=(self.parliamentarian_id), commission_id=self.commission_id, party_id=self.party_id, plenary_date=None, ) ), ) ] for d, in plenary_dates_query: plenary_filters.append( Link( text=layout.format_date(d, 'date'), active=self.plenary_date == d, url=request.link( self.for_filter( settlement_run_id=(self.settlement_run_id), date_from=self.date_from, date_to=self.date_to, type=self.type, parliamentarian_id=(self.parliamentarian_id), commission_id=(self.commission_id), party_id=self.party_id, plenary_date=d, ) ), ) ) allowance_q = PresidentialAllowanceCollection( request.session, settlement_run_id=uuid.UUID(self.settlement_run_id) if self.settlement_run_id else None, ).query() if request.is_parliamentarian: parl = request.current_parliamentarian if parl: allowance_q = allowance_q.filter( PresidentialAllowance.parliamentarian_id == parl.id ) else: allowance_q = allowance_q.filter(false()) allowances = allowance_q.all() edit_links: dict[uuid.UUID, str] = {} editable_groups: dict[uuid.UUID, bool] = {} for a in attendences_sorted: group_key = a.bulk_edit_id or a.id if group_key not in editable_groups: editable_groups[group_key] = request.can_edit_attendence(a) if not editable_groups[group_key]: continue if a.bulk_edit_id: name = ( 'edit-plenary-bulk-attendences' if a.type == 'plenary' else 'edit-commission-bulk-attendences' ) edit_links[a.id] = request.link(a, name) else: edit_links[a.id] = request.link(a) return { 'add_link': request.link(self, name='new'), 'layout': layout, 'attendences': attendences_sorted, 'creators': creators, 'edit_links': edit_links, 'title': layout.title, 'bulk_edit_groups': [ group for group in bulk_edit_groups if group[0].id in edit_links ], 'bulk_pdf_links': { group[0].id: request.link(group[0], name='bulk-attendence-pdf') for group in bulk_edit_groups if group[0].id in edit_links }, 'can_edit_attendences': bool(edit_links), 'allowances': allowances, 'filters': { 'settlement_run': run_filters, 'type': type_filters, 'plenary_session': plenary_filters, }, }
@PasApp.view( model=Attendence, name='bulk-attendence-pdf', permission=Private, request_method='GET', )
[docs] def view_bulk_attendence_pdf( self: Attendence, request: PasRequest, ) -> Response: if not self.bulk_edit_id or not request.can_edit_attendence(self): raise HTTPNotFound() attendances = ( request.session.query(Attendence) .filter( Attendence.bulk_edit_id == self.bulk_edit_id, Attendence.type == self.type, ) .order_by(Attendence.parliamentarian_id) .all() ) date_label = self.date.strftime('%d.%m.%Y') html = render_template( 'bulk_attendence_pdf.pt', request, { 'attendances': attendances, 'date_label': date_label, 'duration': self.duration, 'type_label': request.translate(_(self.type_label)), 'commission': self.commission.title if self.commission else None, }, ) font_config = FontConfiguration() css_path = module_path( 'onegov.pas', 'views/templates/bulk_attendence_pdf.css' ) with open(css_path) as css_file: css = CSS(string=css_file.read()) logo_path = module_path('onegov.agency', 'static/logos/canton-zg-bw.svg') logo_css = CSS( string=f""" @page {{ @top-left {{ content: url('file://{logo_path}'); width: 3cm; }} }} """ ) pdf = HTML(string=str(html)).write_pdf( stylesheets=[css, logo_css], font_config=font_config, ) filename = normalize_for_filename(f'Massenbuchung_{self.date.isoformat()}') return Response( pdf, content_type='application/pdf', content_disposition=f'attachment; filename={filename}.pdf', )
@PasApp.form( model=AttendenceCollection, name='new', template='form.pt', form=AttendenceAddForm )
[docs] def add_attendence( self: AttendenceCollection, request: PasRequest, form: AttendenceAddForm ) -> RenderData | Response: request.include('custom') if form.submitted(request): if form.date.data: if error := validate_attendance_date( request.session, form.date.data ): request.alert(error) return { 'layout': AttendenceCollectionLayout(self, request), 'title': _('New attendence'), 'form': form, 'form_width': 'large', } if not request.is_admin: if ( form.parliamentarian_id.data and form.commission_id.data and form.date.data and has_user_set_abschluss_for_commission( request.session, form.parliamentarian_id.data, form.commission_id.data, form.date.data, ) ): request.alert( _( 'Cannot book attendance - abschluss ' 'already set for this settlement run' ) ) return { 'layout': AttendenceCollectionLayout(self, request), 'title': _('New attendence'), 'form': form, 'form_width': 'large', } attendence = self.add(**form.get_useful_data()) Change.add(request, 'add', attendence) request.success(_('Added a new attendence')) return request.redirect(request.link(attendence)) layout = AttendenceCollectionLayout(self, request) layout.breadcrumbs.append(Link(_('New'), '#')) layout.edit_mode = True return { 'layout': layout, 'title': _('New attendence'), 'form': form, 'form_width': 'large' }
[docs] def handle_commission_bulk_add( self: AttendenceCollection, request: PasRequest, form: AttendenceAddCommissionBulkForm, title: str, ) -> RenderData | Response: """ Shared handler for the bulk add forms which are bound to a single commission (commission sessions and file studies). """ request.include('custom') if not request.is_admin and not request.has_commission_president_role: request.alert(_('Only admins can add bulk attendance records.')) return request.redirect(request.class_link(AttendenceCollection)) if form.submitted(request): if form.date.data: if error := validate_attendance_date( request.session, form.date.data ): request.alert(error) return { 'layout': AttendenceCollectionLayout(self, request), 'title': title, 'form': form, 'form_width': 'large', } data = form.get_useful_data() if raw_parl_ids := request.POST.getall('parliamentarian_id'): if not request.is_admin and form.commission_id.data: blocked_parls: list[str] = [] commission_id = form.commission_id.data user = ( request.session.query(User) .filter_by(username=request.identity.userid) .first() ) today = date.today() led_commission_ids = set() if user and user.parliamentarian: # type: ignore[attr-defined] led_commission_ids = { membership.commission_id for membership in ( user.parliamentarian.commission_memberships_on( # type: ignore[attr-defined] on_date=today, role='president', ) ) } valid_membership_ids = set() if commission_id in led_commission_ids: valid_membership_ids = { membership.parliamentarian_id for membership in request.session.query( PASCommissionMembership ).filter_by(commission_id=commission_id) if membership.is_active_on(today) } for parl_id in raw_parl_ids: if not isinstance(parl_id, str): blocked_parls.append(_('a selected parliamentarian')) break try: pid = UUID(parl_id) except ValueError: blocked_parls.append(_('a selected parliamentarian')) break if commission_id not in led_commission_ids: blocked_parls.append(_('the selected commission')) break assert commission_id is not None if pid not in valid_membership_ids: blocked_parls.append(_('a selected parliamentarian')) break if ( form.date.data and has_user_set_abschluss_for_commission( request.session, pid, commission_id, form.date.data, ) ): parl = PASParliamentarianCollection(request.app).by_id( pid ) if parl: blocked_parls.append(parl.title) if blocked_parls: request.alert( _( 'Cannot book attendance for the selected ' 'commission or parliamentarian.' ) ) return { 'layout': AttendenceCollectionLayout(self, request), 'title': title, 'form': form, 'form_width': 'large', } data.pop('parliamentarian_id', None) bulk_edit_id = uuid.uuid4() notify_attendence = None for parliamentarian_id in raw_parl_ids: attendence = self.add( parliamentarian_id=parliamentarian_id, **data, ) attendence.bulk_edit_id = bulk_edit_id Change.add(request, 'add', attendence) if attendence.abschluss and notify_attendence is None: notify_attendence = attendence if notify_attendence is not None: notify_admins_finalized(request, notify_attendence) else: request.warning(_('No parliamentarians selected')) return request.redirect(request.class_link(AttendenceCollection)) request.success(_('Added session')) return request.redirect(request.link(self)) layout = AttendenceCollectionLayout(self, request) layout.breadcrumbs.append(Link(title, '#')) layout.edit_mode = True return { 'layout': layout, 'title': title, 'form': form, 'form_width': 'large', }
@PasApp.form( model=AttendenceCollection, name='new-commission-bulk', template='form.pt', permission=Private, form=AttendenceAddCommissionBulkForm )
[docs] def add_bulk_attendence( self: AttendenceCollection, request: PasRequest, form: AttendenceAddCommissionBulkForm ) -> RenderData | Response: return handle_commission_bulk_add( self, request, form, _('New commission session (bulk)') )
@PasApp.form( model=AttendenceCollection, name='new-file-study-bulk', template='form.pt', permission=Private, form=AttendenceAddFileStudyBulkForm )
[docs] def add_file_study_bulk_attendence( self: AttendenceCollection, request: PasRequest, form: AttendenceAddFileStudyBulkForm ) -> RenderData | Response: return handle_commission_bulk_add( self, request, form, _('New file study (bulk)') )
@PasApp.form( model=Attendence, name='edit-plenary-bulk-attendences', template='form.pt', permission=Private, form=AttendencePlenaryBulkEditForm )
[docs] def edit_plenary_bulk_attendence( self: Attendence, request: TownRequest, form: AttendencePlenaryBulkEditForm ) -> RenderData | Response: request.include('custom') if not request.is_admin: request.alert( _('You do not have permission to edit plenary sessions.') ) return request.redirect(request.class_link(AttendenceCollection)) all_parliamentarians = [ p.id for p in get_attendance_parliamentarians(request) ] if form.submitted(request): # Check if attendance date is in a closed settlement run if form.date.data: if error := validate_attendance_date( request.session, form.date.data ): request.alert(error) return { 'layout': AttendenceCollectionLayout(self, request), 'title': _('Edit bulk: plenary session'), 'form': form, 'form_width': 'large' } data = form.get_useful_data() parl_ids = data.pop('parliamentarian_id', ()) collection = AttendenceCollection(request.session) unselected_parliamentarians = [ pid for pid in all_parliamentarians if pid not in parl_ids ] for parliamentarian in unselected_parliamentarians: if attendence := collection.query().filter( Attendence.parliamentarian_id == parliamentarian, Attendence.bulk_edit_id == form.bulk_edit_id.data, ).first(): Change.add(request, 'delete', attendence) collection.delete(attendence) for parliamentarian_id in parl_ids: attendence = collection.query().filter( Attendence.parliamentarian_id == parliamentarian_id, Attendence.bulk_edit_id == form.bulk_edit_id.data, ).first() if attendence: form.populate_obj(attendence) Change.add(request, 'edit', attendence) else: attendence = collection.add( parliamentarian_id=parliamentarian_id, **data ) Change.add(request, 'add', attendence) request.success(_('Edited attendences')) return request.redirect(request.class_link(AttendenceCollection)) elif not request.POST: form.process(obj=self) layout = AttendenceCollectionLayout(self, request) layout.breadcrumbs = [ Link(_('Homepage'), layout.homepage_url), Link(_('Attendences'), request.class_link(AttendenceCollection)), Link(_('Edit session'), '#') ] layout.edit_mode = True layout.editmode_links[1] = BackLink( attrs={'class': 'cancel-link'}, ) layout.editmode_links.append( Link( text=_('Delete'), url=layout.csrf_protected_url( request.link(self, name='delete-attendences')), traits=( Confirm( _( 'Do you really want to delete this bulk edit?' ), _('This cannot be undone.'), _('Delete Attendencess'), _('Cancel') ), Intercooler( request_method='DELETE', redirect_after=request.class_link(AttendenceCollection) )), attrs={'class': 'delete-link'}, ) ) return { 'layout': layout, 'title': _('Edit bulk: plenary session'), 'form': form, 'form_width': 'large' }
@PasApp.form( model=Attendence, name='edit-commission-bulk-attendences', template='form.pt', permission=Private, form=AttendenceCommissionBulkEditForm )
[docs] def edit_commission_bulk_attendence( self: Attendence, request: PasRequest, form: AttendenceCommissionBulkEditForm ) -> RenderData | Response: request.include('custom') if not self.bulk_edit_id or not request.can_edit_attendence(self): request.alert(_('Insufficient permissions.')) return request.redirect(request.class_link(AttendenceCollection)) type_label = request.translate(self.type_label) title = _('Edit bulk: ${type}', mapping={'type': type_label}) if form.submitted(request): if not request.is_admin: assert form.date.data is not None if ( form.bulk_edit_id.data != str(self.bulk_edit_id) or form.commission_id.data != self.commission_id or not all( request.can_edit_commission_attendance( form.date.data, self.commission_id, person_id ) for person_id in (form.parliamentarian_id.data or []) ) ): request.alert(_('Insufficient permissions.')) return request.redirect( request.class_link(AttendenceCollection) ) if form.date.data: if error := validate_attendance_date( request.session, form.date.data ): request.alert(error) return { 'layout': AttendenceCollectionLayout(self, request), 'title': title, 'form': form, 'form_width': 'large' } data = form.get_useful_data() # Newly added attendences inherit the type of the bulk group data['type'] = self.type if parl_ids := form.parliamentarian_id.data: data.pop('parliamentarian_id', None) collection = AttendenceCollection(request.session) memberships = ( request.session.query(PASCommissionMembership) .filter( PASCommissionMembership.commission_id == form.commission_id.data ) .all() ) eligible = {p.id for p in get_attendance_parliamentarians(request)} commission_parliamentarians = [ membership.parliamentarian_id for membership in memberships if membership.parliamentarian_id in eligible ] unselected_parliamentarians = [ pid for pid in commission_parliamentarians if pid not in parl_ids ] for parliamentarian in unselected_parliamentarians: if attendence := collection.query().filter( Attendence.parliamentarian_id == parliamentarian, Attendence.bulk_edit_id == self.bulk_edit_id, ).first(): Change.add(request, 'delete', attendence) collection.delete(attendence) notify_attendence = None for parliamentarian_id in parl_ids: attendence = collection.query().filter( Attendence.parliamentarian_id == parliamentarian_id, Attendence.bulk_edit_id == self.bulk_edit_id, ).first() if attendence: was_abschluss = attendence.abschluss form.populate_obj(attendence) Change.add(request, 'edit', attendence) if ( attendence.abschluss and not was_abschluss and notify_attendence is None ): notify_attendence = attendence else: attendence = collection.add( parliamentarian_id=parliamentarian_id, **data ) attendence.bulk_edit_id = self.bulk_edit_id Change.add(request, 'add', attendence) if attendence.abschluss and notify_attendence is None: notify_attendence = attendence if notify_attendence is not None: notify_admins_finalized(request, notify_attendence) request.success(_('Edited attendences')) return request.redirect(request.class_link(AttendenceCollection)) elif not request.POST: form.process(obj=self) layout = AttendenceCollectionLayout(self, request) layout.breadcrumbs = [ Link(_('Homepage'), layout.homepage_url), Link(_('Attendences'), request.class_link(AttendenceCollection)), Link(_('Edit session'), '#') ] layout.edit_mode = True layout.editmode_links[1] = BackLink( attrs={'class': 'cancel-link'}, ) if request.is_admin: layout.editmode_links.append( Link( text=_('Delete'), url=layout.csrf_protected_url( request.link(self, name='delete-attendences') ), traits=( Confirm( _('Do you really want to delete this bulk edit?'), _('This cannot be undone.'), _('Delete Attendencess'), _('Cancel'), ), Intercooler( request_method='DELETE', redirect_after=request.class_link( AttendenceCollection ), ), ), attrs={'class': 'delete-link'}, ) ) return { 'layout': layout, 'title': title, 'form': form, 'form_width': 'large' }
@PasApp.form( model=AttendenceCollection, name='new-bulk', template='form.pt', form=AttendenceAddPlenaryForm )
[docs] def add_plenary_attendence(self: AttendenceCollection, request: PasRequest, form: AttendenceAddPlenaryForm ) -> RenderData | Response: request.include('custom') if not request.is_admin: request.alert(_('You do not have permission to add plenary sessions.')) return request.redirect(request.link(self)) if form.submitted(request): # Check if attendance date is in a closed settlement run if form.date.data: if error := validate_attendance_date( request.session, form.date.data ): request.alert(error) return { 'layout': AttendenceCollectionLayout(self, request), 'title': _('New plenary session'), 'form': form, 'form_width': 'large' } data = form.get_useful_data() # Remove static field; choices are set dynamically via JS if parl_ids := data.pop('parliamentarian_id', None): bulk_edit_id = uuid.uuid4() for parliamentarian_id in parl_ids: attendence = self.add( parliamentarian_id=parliamentarian_id, **data, ) attendence.bulk_edit_id = bulk_edit_id Change.add(request, 'add', attendence) request.success(_('Added plenary session')) return request.redirect(request.link(self)) layout = AttendenceCollectionLayout(self, request) layout.breadcrumbs.append(Link(_('New plenary session'), '#')) layout.edit_mode = True return { 'layout': layout, 'title': _('New plenary session'), 'form': form, 'form_width': 'large' }
@PasApp.html( model=Attendence, template='attendence.pt', permission=Private )
[docs] def view_attendence( self: Attendence, request: PasRequest ) -> RenderData: layout = AttendenceLayout(self, request) return { 'layout': layout, 'attendence': self, 'title': layout.title, }
@PasApp.form( model=Attendence, name='edit', template='form.pt', form=AttendenceForm, permission=Private )
[docs] def edit_attendence( self: Attendence, request: PasRequest, form: AttendenceForm ) -> RenderData | Response: if not request.can_edit_attendence(self): request.alert(_('Insufficient permissions.')) return request.redirect(request.class_link(AttendenceCollection)) if self.bulk_edit_id: name = ( 'edit-plenary-bulk-attendences' if self.type == 'plenary' else 'edit-commission-bulk-attendences' ) return request.redirect(request.link(self, name)) if form.submitted(request): if not request.is_admin: assert form.date.data is not None if ( form.type.data == 'plenary' or form.commission_id.data != self.commission_id or form.parliamentarian_id.data != self.parliamentarian_id or not request.can_edit_commission_attendance( form.date.data, self.commission_id, self.parliamentarian_id, ) ): request.alert(_('Insufficient permissions.')) return request.redirect( request.class_link(AttendenceCollection) ) if form.date.data: if error := validate_attendance_date( request.session, form.date.data ): request.alert(error) return { 'layout': AttendenceCollectionLayout(self, request), 'title': _('New plenary session'), 'form': form, 'form_width': 'large' } was_abschluss = self.abschluss form.populate_obj(self) Change.add(request, 'edit', self) if self.abschluss and not was_abschluss: notify_admins_finalized(request, self) request.success(_('Your changes were saved')) return request.redirect(request.link(self)) form.process(obj=self) layout = AttendenceLayout(self, request) layout.breadcrumbs.append(Link(_('Edit'), '#')) layout.edit_mode = True return { 'layout': layout, 'title': layout.title, 'form': form, 'form_width': 'large' }
@PasApp.view( model=Attendence, request_method='DELETE', )
[docs] def delete_attendence( self: Attendence, request: PasRequest ) -> None: request.assert_valid_csrf_token() if not request.is_admin: request.alert(_('Only admins can delete attendance records.')) return if self.bulk_edit_id: request.alert(_('Cannot delete individual bulk attendance.')) return # Check if attendance is in a closed settlement run settlement_run = request.session.query(SettlementRun).filter( SettlementRun.start <= self.date, SettlementRun.end >= self.date, SettlementRun.closed == True ).first() if settlement_run: request.alert( _('Cannot delete attendance in closed settlement run.') ) return Change.add(request, 'delete', self) collection = AttendenceCollection(request.session) collection.delete(self)
@PasApp.view( model=Attendence, name='delete-attendences', request_method='DELETE', permission=Private )
[docs] def delete_attendences( self: Attendence, request: TownRequest ) -> None: request.assert_valid_csrf_token() if not request.is_admin: request.alert(_('Only admins can delete attendance records.')) return attendences = request.session.query(Attendence).filter( Attendence.bulk_edit_id == self.bulk_edit_id ).all() # Check if any attendance is in a closed settlement run for attendence in attendences: settlement_run = request.session.query(SettlementRun).filter( SettlementRun.start <= attendence.date, SettlementRun.end >= attendence.date, SettlementRun.closed == True ).first() if settlement_run: request.alert( _('Cannot delete attendance in closed settlement run.') ) return for attendence in attendences: Change.add(request, 'delete', attendence) collection = AttendenceCollection(request.session) collection.delete(attendence) request.success(_('Deleted ${count} attendeces', mapping={ 'count': len(attendences) }))