Source code for translator_directory.views.translator

from __future__ import annotations

from decimal import Decimal
from io import BytesIO
from onegov.file import File
from morepath import redirect
from sedate import replace_timezone
from datetime import datetime
from morepath.request import Response
from sedate import utcnow
from onegov.core.custom import json
from onegov.core.security import Secret, Personal, Private
from onegov.core.templates import render_template
from onegov.file.integration import get_file
from onegov.org.layout import DefaultMailLayout
from onegov.org.mail import send_ticket_mail
from onegov.org.models import GeneralFileCollection
from onegov.org.models import TicketMessage
from onegov.org.utils import emails_for_new_ticket
from onegov.ticket import TicketCollection
from onegov.translator_directory import _
from onegov.translator_directory import TranslatorDirectoryApp
from onegov.translator_directory.collections.translator import (
    TranslatorCollection)
from onegov.translator_directory.constants import (
    PROFESSIONAL_GUILDS, INTERPRETING_TYPES, ADMISSIONS, GENDERS, GENDER_MAP)
from onegov.translator_directory.forms.mutation import TranslatorMutationForm
from onegov.translator_directory.forms.time_report import (
    TranslatorTimeReportForm,
)
from onegov.translator_directory.forms.translator import (
    TranslatorForm, TranslatorSearchForm,
    EditorTranslatorForm, MailTemplatesForm)
from onegov.translator_directory.generate_docx import (
    fill_docx_with_variables, signature_for_mail_templates,
    parse_from_filename, get_ticket_nr_of_translator)
from onegov.translator_directory.layout import (
    AddTranslatorLayout,
    TranslatorCollectionLayout,
    TranslatorLayout,
    EditTranslatorLayout,
    ReportTranslatorChangesLayout,
    MailTemplatesLayout,
)
from onegov.translator_directory.models.time_report import TranslatorTimeReport
from onegov.translator_directory.models.translator import Translator
from onegov.translator_directory.utils import country_code_to_name
from onegov.translator_directory.utils import get_accountant_email

from uuid import uuid4
from xlsxwriter import Workbook
from docx.image.exceptions import UnrecognizedImageError
from webob.exc import HTTPForbidden


from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from datetime import date, datetime
    from collections.abc import Iterable
    from onegov.core.types import RenderData
    from onegov.translator_directory.models.certificate import (
        LanguageCertificate)
    from onegov.translator_directory.models.language import Language
    from onegov.translator_directory.models.translator import (
        AdmissionState, Gender, InterpretingType)
    from onegov.translator_directory.request import TranslatorAppRequest
    from webob import Response as BaseResponse


@TranslatorDirectoryApp.form(
    model=TranslatorCollection,
    template='form.pt',
    name='new',
    form=TranslatorForm,
    permission=Secret
)
[docs] def add_new_translator( self: TranslatorCollection, request: TranslatorAppRequest, form: TranslatorForm ) -> RenderData | BaseResponse: form.delete_field('date_of_decision') form.delete_field('for_admins_only') form.delete_field('proof_of_preconditions') if form.submitted(request): data = form.get_useful_data() translator = self.add(**data) request.success(_('Added a new translator')) if translator.user: subject = request.translate( _('An account was created for you') ) content = render_template('mail_new_user.pt', request, { 'user': translator.user, 'org': request.app.org, 'layout': DefaultMailLayout(translator.user, request), 'title': subject }) request.app.send_transactional_email( subject=subject, receivers=(translator.user.username, ), content=content, ) request.success(_('Activation E-Mail sent')) return request.redirect(request.link(translator)) layout = AddTranslatorLayout(self, request) layout.edit_mode = True return { 'layout': layout, 'model': self, 'form': form, 'title': layout.title }
@TranslatorDirectoryApp.form( model=TranslatorCollection, template='translators.pt', permission=Personal, form=TranslatorSearchForm )
[docs] def view_translators( self: TranslatorCollection, request: TranslatorAppRequest, form: TranslatorSearchForm ) -> RenderData | BaseResponse: layout = TranslatorCollectionLayout(self, request) if form.submitted(request): form.update_model(self) return request.redirect(request.link(self)) if not form.errors: form.apply_model(self) return { 'layout': layout, 'model': self, 'title': layout.title, 'form': form, 'results': self.batch, 'button_text': _('Submit Search') }
@TranslatorDirectoryApp.view( model=TranslatorCollection, permission=Secret, name='export' )
[docs] def export_translator_directory( self: TranslatorCollection, request: TranslatorAppRequest ) -> Response: output = BytesIO() workbook = Workbook(output) def format_date(dt: datetime | date | None) -> str: if not dt: return '' return dt.strftime('%Y-%m-%d') def format_iterable(listlike: Iterable[str]) -> str: return '|'.join(listlike) if listlike else '' def format_languages( langs: Iterable[Language | LanguageCertificate] ) -> str: return format_iterable(la.name for la in langs) def format_guilds(guilds: Iterable[str]) -> str: return format_iterable( request.translate(PROFESSIONAL_GUILDS[s]) if s in PROFESSIONAL_GUILDS else s for s in guilds ) def format_interpreting_types(types: Iterable[InterpretingType]) -> str: return format_iterable( request.translate(INTERPRETING_TYPES[t]) for t in types ) def format_admission(admission: AdmissionState | None) -> str: if not admission: return '' return request.translate(ADMISSIONS[admission]) def format_gender(gender: Gender | None) -> str: if not gender: return '' return request.translate(GENDERS[gender]) def format_nationalities(nationalities: list[str] | None) -> str: mapping = country_code_to_name(request.locale) if not nationalities: return '' return ', '.join(mapping[n] for n in nationalities) worksheet = workbook.add_worksheet( request.translate(_('Translator directory')) ) worksheet.write_row(0, 0, ( request.translate(_('Personal ID')), request.translate(_('Admission')), request.translate(_('Withholding tax')), request.translate(_('Self-employed')), request.translate(_('Last name')), request.translate(_('First name')), request.translate(_('Gender')), request.translate(_('Date of birth')), request.translate(_('Nationality(ies)')), request.translate(_('Location')), request.translate(_('Address')), request.translate(_('Zip Code')), request.translate(_('City')), request.translate(_('Drive distance')), request.translate(_('Swiss social security number')), request.translate(_('Bank name')), request.translate(_('Bank address')), request.translate(_('Account owner')), request.translate(_('IBAN')), request.translate(_('Email')), request.translate(_('Private Phone Number')), request.translate(_('Mobile Phone Number')), request.translate(_('Office Phone Number')), request.translate(_('Availability')), request.translate(_('Comments on possible field of application')), request.translate(_('Name reveal confirmation')), request.translate(_('Date of application')), request.translate(_('Date of decision')), request.translate(_('Mother tongues')), request.translate(_('Spoken languages')), request.translate(_('Written languages')), request.translate(_('Monitoring languages')), request.translate(_('Learned profession')), request.translate(_('Current professional activity')), request.translate(_('Expertise by professional guild')), request.translate(_('Expertise by interpreting type')), request.translate(_('Proof of preconditions')), request.translate(_('Agency references')), request.translate(_('Education as interpreter')), request.translate(_('Certificates')), request.translate(_('Comments')), request.translate(_('Hidden')), )) for ix, trs in enumerate(self.query()): worksheet.write_row(ix + 1, 0, ( trs.pers_id or '', format_admission(trs.admission), trs.withholding_tax and 1 or 0, trs.self_employed and 1 or 0, trs.last_name, trs.first_name, format_gender(trs.gender), format_date(trs.date_of_birth), format_nationalities(trs.nationalities), json.dumps(trs.coordinates), trs.address or '', trs.zip_code or '', trs.city or '', trs.drive_distance or '', trs.social_sec_number or '', trs.bank_name or '', trs.bank_address or '', trs.account_owner or '', trs.iban or '', trs.email or '', trs.tel_private or '', trs.tel_mobile or '', trs.tel_office or '', trs.availability or '', trs.operation_comments or '', trs.confirm_name_reveal and 1 or 0, format_date(trs.date_of_application), format_date(trs.date_of_decision), format_languages(trs.mother_tongues), format_languages(trs.spoken_languages), format_languages(trs.written_languages), format_languages(trs.monitoring_languages), trs.profession or '', trs.occupation or '', format_guilds(trs.expertise_professional_guilds_all), format_interpreting_types(trs.expertise_interpreting_types), trs.proof_of_preconditions or '', trs.agency_references or '', trs.education_as_interpreter and 1 or 0, format_languages(trs.certificates), trs.comments or '', trs.for_admins_only and 1 or 0 )) workbook.close() output.seek(0) response = Response() response.content_type = ( 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response.content_disposition = 'inline; filename={}-{}.xlsx'.format( request.translate(_('Translator directory')).lower(), utcnow().strftime('%Y%m%d%H%M') ) response.body = output.read() return response
@TranslatorDirectoryApp.html( model=Translator, template='translator.pt', permission=Personal )
[docs] def view_translator( self: Translator, request: TranslatorAppRequest ) -> RenderData: layout = TranslatorLayout(self, request) if layout.translator_data_outdated(): request.warning(_( 'Is your information still up do date? Please check and either ' 'modify or confirm using the buttons above.')) return { 'layout': layout, 'model': self, 'title': self.title, }
@TranslatorDirectoryApp.form( model=Translator, template='form.pt', name='edit', form=TranslatorForm, permission=Secret )
[docs] def edit_translator( self: Translator, request: TranslatorAppRequest, form: TranslatorForm ) -> RenderData | BaseResponse: if form.submitted(request): form.update_model(self) request.success(_('Your changes were saved')) return request.redirect(request.link(self)) if not form.errors: form.process( obj=self, data={ attr: form.get_ids(self, attr.removesuffix('_ids')) for attr in form.special_fields } ) layout = EditTranslatorLayout(self, request) layout.edit_mode = True if not self.coordinates: request.warning( _('Translator is lacking location and address.') ) return { 'layout': layout, 'model': self, 'form': form, 'title': layout.title }
@TranslatorDirectoryApp.form( model=Translator, template='form.pt', name='edit-restricted', form=EditorTranslatorForm, permission=Private )
[docs] def edit_translator_as_editor( self: Translator, request: TranslatorAppRequest, form: EditorTranslatorForm ) -> RenderData | BaseResponse: if request.is_admin: return request.redirect(request.link(self, name='edit')) if form.submitted(request): form.update_model(self) request.success(_('Your changes were saved')) return request.redirect(request.link(self)) if not form.errors: form.process(obj=self) layout = EditTranslatorLayout(self, request) return { 'layout': layout, 'model': self, 'form': form, 'title': layout.title }
@TranslatorDirectoryApp.view( model=Translator, request_method='DELETE', permission=Secret )
[docs] def delete_translator( self: Translator, request: TranslatorAppRequest ) -> None: request.assert_valid_csrf_token() TranslatorCollection(request.app).delete(self) request.success(_('Translator successfully deleted'))
@TranslatorDirectoryApp.form( model=Translator, name='report-change', template='form.pt', permission=Personal, form=TranslatorMutationForm )
[docs] def report_translator_change( self: Translator, request: TranslatorAppRequest, form: TranslatorMutationForm ) -> RenderData | BaseResponse: if request.is_member: raise HTTPForbidden() if form.submitted(request): assert request.current_username is not None session = request.session # Get uploaded files from the form uploaded_files = form.get_files() file_ids: list[str] = [] if uploaded_files: self.files.extend(uploaded_files) session.flush() file_ids = [f.id for f in uploaded_files] with session.no_autoflush: ticket = TicketCollection(session).open_ticket( handler_code='TRN', handler_id=uuid4().hex, handler_data={ 'id': str(self.id), 'submitter_email': request.current_username, 'submitter_message': form.submitter_message.data, 'proposed_changes': form.proposed_changes, 'file_ids': file_ids, }, ) TicketMessage.create(ticket, request, 'opened', 'external') ticket.create_snapshot(request) send_ticket_mail( request=request, template='mail_ticket_opened.pt', subject=_('Your ticket has been opened'), receivers=(request.current_username, ), ticket=ticket, send_self=True ) for email in emails_for_new_ticket(request, ticket): send_ticket_mail( request=request, template='mail_ticket_opened_info.pt', subject=_('New ticket'), ticket=ticket, receivers=(email, ), content={'model': ticket}, ) request.app.send_websocket( channel=request.app.websockets_private_channel, message={ 'event': 'browser-notification', 'title': request.translate(_('New ticket')), 'created': ticket.created.isoformat() }, groupids=request.app.groupids_for_ticket(ticket), ) request.success(_('Thank you for your submission!')) return redirect(request.link(ticket, 'status')) layout = ReportTranslatorChangesLayout(self, request) return { 'layout': layout, 'title': layout.title, 'form': form }
@TranslatorDirectoryApp.view( model=Translator, name='confirm-current-data', permission=Personal, )
[docs] def confirm_current_data( self: Translator, request: TranslatorAppRequest ) -> BaseResponse: TranslatorCollection(request.app).confirm_current_data(self) request.success(_('Your data has been confirmed')) return redirect(request.link(self))
@TranslatorDirectoryApp.form( model=Translator, template='form.pt', name='add-time-report', form=TranslatorTimeReportForm, permission=Personal, )
[docs] def add_time_report( self: Translator, request: TranslatorAppRequest, form: TranslatorTimeReportForm, ) -> RenderData | BaseResponse: if form.submitted(request): try: accountant_email = get_accountant_email(request) except ValueError as e: request.alert(str(e)) layout = TranslatorLayout(self, request) layout.edit_mode = True return { 'layout': layout, 'model': self, 'form': form, 'title': _('Add Time Report'), } assert request.current_username is not None assert form.start_date.data is not None assert form.start_time.data is not None assert form.end_date.data is not None assert form.end_time.data is not None session = request.session current_user = request.current_user duration_hours = form.get_duration_hours() duration_minutes = int(float(duration_hours) * 60) hourly_rate = form.get_hourly_rate(self) surcharge_types = form.get_surcharge_types() travel_comp, travel_distance = form.calculate_travel_details( self, request ) start_dt = datetime.combine(form.start_date.data, form.start_time.data) end_dt = datetime.combine(form.end_date.data, form.end_time.data) start_dt = replace_timezone(start_dt, 'Europe/Zurich') end_dt = replace_timezone(end_dt, 'Europe/Zurich') # Calculate break time in minutes break_minutes = 0 if form.break_time.data: break_minutes = ( form.break_time.data.hour * 60 + form.break_time.data.minute ) # Calculate night hours (in minutes) night_hours = form.calculate_night_hours() night_minutes = int(float(night_hours) * 60) # Calculate weekend/holiday hours weekend_holiday_hours = form.calculate_weekend_holiday_hours() weekend_holiday_minutes = int(float(weekend_holiday_hours) * 60) # Create report with all fields except total_compensation report = TranslatorTimeReport( translator=self, created_by=current_user, assignment_type=form.assignment_type.data or None, assignment_location=form.assignment_location.data or None, duration=duration_minutes, break_time=break_minutes, night_minutes=night_minutes, weekend_holiday_minutes=weekend_holiday_minutes, case_number=form.case_number.data or None, assignment_date=form.start_date.data, start=start_dt, end=end_dt, hourly_rate=hourly_rate, surcharge_types=surcharge_types if surcharge_types else None, travel_compensation=travel_comp, travel_distance=travel_distance, # Temporary, will be calculated next total_compensation=Decimal('0'), notes=form.notes.data or None, ) # Use centralized calculation from model breakdown = report.calculate_compensation_breakdown() report.total_compensation = breakdown['total'] session.add(report) session.flush() with session.no_autoflush: ticket = TicketCollection(session).open_ticket( handler_code='TRP', handler_id=uuid4().hex, handler_data={ 'translator_id': str(self.id), 'submitter_email': request.current_username, 'time_report_id': str(report.id), }, ) TicketMessage.create(ticket, request, 'opened', 'external') ticket.create_snapshot(request) send_ticket_mail( request=request, template='mail_ticket_opened.pt', subject=_('Your ticket has been opened'), receivers=(request.current_username,), ticket=ticket, send_self=True, ) for email in emails_for_new_ticket(request, ticket): if email != accountant_email: send_ticket_mail( request=request, template='mail_ticket_opened_info.pt', subject=_('New ticket'), ticket=ticket, receivers=(email,), content={'model': ticket}, ) send_ticket_mail( request=request, template='mail_time_report_created.pt', subject=_('New time report for review'), ticket=ticket, receivers=(accountant_email,), content={ 'model': ticket, 'translator': self, 'time_report': report, }, ) request.app.send_websocket( channel=request.app.websockets_private_channel, message={ 'event': 'browser-notification', 'title': request.translate(_('New ticket')), 'created': ticket.created.isoformat(), }, groupids=request.app.groupids_for_ticket(ticket), ) request.success(_('Time report submitted for review')) return redirect(request.link(ticket, 'status')) layout = TranslatorLayout(self, request) return { 'layout': layout, 'model': self, 'form': form, 'title': _('Add Time Report'), 'button_text': request.translate(_('Submit Time Report')), }
@TranslatorDirectoryApp.form( model=Translator, template='mail_templates.pt', name='mail-templates', form=MailTemplatesForm, permission=Personal )
[docs] def view_mail_templates( self: Translator, request: TranslatorAppRequest, form: MailTemplatesForm ) -> RenderData | BaseResponse: layout = MailTemplatesLayout(self, request) if form.submitted(request): template_name = form.templates.data if template_name not in request.app.mail_templates: request.alert(_('This file does not seem to exist.')) return redirect(request.link(self)) user = request.current_user assert user is not None if not user.realname: request.alert( request.translate( _( 'Unfortunately, this account (${account}) does not ' 'have real name defined, which is required for mail ' 'templates', mapping={'account': user.username}, ) ) ) return redirect(request.link(self)) signature_file = signature_for_mail_templates(request) if not signature_file: request.alert(_('Did not find a signature in /files.')) return redirect(request.link(self)) signature_file_name = parse_from_filename(signature_file.name) first_name, last_name = user.realname.split(' ') additional_fields = { 'current_date': layout.format_date(utcnow(), 'date'), 'translator_date_of_birth': layout.format_date( self.date_of_birth, 'date'), 'translator_date_of_decision': layout.format_date( self.date_of_decision, 'date' ), 'translator_gender': ( request.translate(GENDER_MAP[self.gender]) if self.gender else '' ), 'translator_admission': request.translate(_(self.admission)) or '', 'sender_first_name': first_name, 'sender_last_name': last_name, 'sender_full_name': signature_file_name.sender_full_name, 'sender_function': signature_file_name.sender_function, 'sender_abbrev': signature_file_name.sender_abbrev, 'translator_hometown': self.hometown if self.hometown else '', 'translator_ticket_number': get_ticket_nr_of_translator( self, request ), } docx_template_id = ( GeneralFileCollection(request.session) .query() .filter(File.name == template_name) .with_entities(File.id) .first() ) docx_f = get_file(request.app, docx_template_id) assert docx_f is not None template = docx_f.reference.file.read() signature_f = get_file(request.app, signature_file.id) assert signature_f is not None signature_bytes = signature_f.reference.file.read() try: __, docx = fill_docx_with_variables( BytesIO(template), self, request, BytesIO(signature_bytes), **additional_fields ) return Response( docx, content_type='application/vnd.ms-office', content_disposition=f'inline; filename={template_name}', ) except UnrecognizedImageError: request.alert(_('The image for the signature could not be ' 'recognized.')) return { 'layout': layout, 'model': self, 'form': form, 'title': _('Mail templates'), 'button_text': _('Download'), }