From c57887b1f03d875937b4699822af6fba55e09753 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Tue, 24 Mar 2026 14:10:16 +0100 Subject: [PATCH] snapshot: preserve bilingual smoke test coverage --- README.md | 1 + backend/workflows/tasks.py | 11 + .../workflows/developer_handbook.html | 1 + .../templates/workflows/project_wiki.html | 1 + .../workflows/tests/test_bilingual_smoke.py | 193 ++++++++++++++++++ backend/workflows/views.py | 4 +- 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 backend/workflows/tests/test_bilingual_smoke.py diff --git a/README.md b/README.md index f525dbf..5fc601d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Notes: - live introduction protocol PDF - Remaining bilingual gap is mostly long-form handbook/wiki copy and a few secondary admin/help texts. - CI now validates that translation catalogs compile successfully on push and pull request. +- Automated bilingual smoke coverage now verifies DE/EN request language capture plus English email-template selection for onboarding and welcome flows. - Dependency stability hardening pins `chardet==5.2.0` so `requests` runs without compatibility warnings in the Docker stack. ## Current implemented scope diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index e8e0a78..22358f0 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -1235,11 +1235,13 @@ def process_onboarding_request(onboarding_request_id: int) -> None: context=email_context, to=[it_email], attachments=[pdf_path], + language_code=request_obj.preferred_language, ) _send_templated_email( template_key='onboarding_general_info', context=email_context, to=[general_info_email], + language_code=request_obj.preferred_language, ) if request_obj.order_business_cards: @@ -1247,6 +1249,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None: template_key='onboarding_business_card', context=email_context, to=[business_card_email], + language_code=request_obj.preferred_language, ) if 'HR Works' in request_obj.needed_accesses: @@ -1254,6 +1257,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None: template_key='onboarding_hr_works', context=email_context, to=[hr_works_email], + language_code=request_obj.preferred_language, ) if 'Schlüssel' in request_obj.needed_devices: @@ -1261,6 +1265,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None: template_key='onboarding_key', context=email_context, to=[key_email], + language_code=request_obj.preferred_language, ) if request_obj.onboarded_by_email: @@ -1269,6 +1274,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None: context=email_context, to=[request_obj.onboarded_by_email], attachments=[pdf_path], + language_code=request_obj.preferred_language, ) _apply_notification_rules( @@ -1305,11 +1311,13 @@ def process_offboarding_request(offboarding_request_id: int) -> None: context=email_context, to=[it_email], attachments=[pdf_path], + language_code=request_obj.preferred_language, ) _send_templated_email( template_key='offboarding_general_info', context=email_context, to=[general_info_email], + language_code=request_obj.preferred_language, ) had_hr_works = OnboardingRequest.objects.filter( @@ -1321,6 +1329,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None: template_key='offboarding_hr_works_disable', context=email_context, to=[hr_works_email], + language_code=request_obj.preferred_language, ) _send_templated_email( @@ -1328,6 +1337,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None: context=email_context, to=[request_obj.requested_by_email], attachments=[pdf_path], + language_code=request_obj.preferred_language, ) _apply_notification_rules( @@ -1389,6 +1399,7 @@ def send_scheduled_welcome_email(scheduled_email_id: int, force_now: bool = Fals to=[scheduled.recipient_email], attachments=attachments, from_email=from_email or None, + language_code=request_obj.preferred_language, ) scheduled.status = 'sent' scheduled.sent_at = timezone.now() diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index ca66e2d..4c6431d 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -195,6 +195,7 @@ docker compose exec -T web python manage.py run_staging_e2e_check
  • Use targeted shell checks for render validation when changing templates or routes.
  • Use real PDF generation tests when changing PDF templates or intro/offboarding document logic.
  • Use the dedicated Release Checklist page as the final go/no-go runbook before shipping changes.
  • +
  • The automated bilingual smoke tests now cover DE/EN request language capture and English email-template rendering.
  • 12) Deployment and Release Checklist

    diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html index 585a1b5..4a8b623 100644 --- a/backend/workflows/templates/workflows/project_wiki.html +++ b/backend/workflows/templates/workflows/project_wiki.html @@ -173,6 +173,7 @@
  • PDF phase added: fixed PDF headings, labels, notes, and confirmation text now render in German or English based on the request language, with German as the fallback.
  • Editing path: these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.
  • Not fully bilingual yet: the main remaining gaps are long-form handbook/wiki copy and a few secondary admin/help texts.
  • +
  • Smoke coverage: automated tests now verify DE/EN request language capture and English email-template selection for onboarding and welcome email flows.
  • Implementation: Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.
  • diff --git a/backend/workflows/tests/test_bilingual_smoke.py b/backend/workflows/tests/test_bilingual_smoke.py new file mode 100644 index 0000000..a7a3f4a --- /dev/null +++ b/backend/workflows/tests/test_bilingual_smoke.py @@ -0,0 +1,193 @@ +from datetime import date +from pathlib import Path +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings +from django.utils import timezone + +from workflows.models import EmployeeProfile, NotificationTemplate, OffboardingRequest, OnboardingRequest, ScheduledWelcomeEmail +from workflows.tasks import process_onboarding_request, send_scheduled_welcome_email + + +@override_settings(PDF_OUTPUT_DIR=Path('/tmp/onoff_test_pdfs')) +class BilingualSmokeTests(TestCase): + def setUp(self): + user_model = get_user_model() + self.user = user_model.objects.create_user( + username='bilingual_user', + password='secret123', + email='requester@tub.co', + first_name='Mia', + last_name='Beispiel', + ) + self.client.force_login(self.user) + self.profile = EmployeeProfile.objects.create( + full_name='Lara Beispiel', + first_name='Lara', + last_name='Beispiel', + department='IT-Service', + job_title='Engineer', + work_email='lara.beispiel@tub.co', + ) + + @patch('workflows.views.process_onboarding_request.delay') + def test_onboarding_submit_persists_english_language(self, mock_delay): + payload = { + 'first_name': 'Max', + 'last_name': 'Mustermann', + 'gender': 'herr', + 'job_title': 'Consultant', + 'department': 'IT-Service', + 'work_email': 'max.mustermann@tub.co', + 'contract_start': '2026-11-01', + 'employment_type': 'unbefristet', + 'group_mailboxes_required_choice': 'nein', + 'additional_hardware_needed_choice': 'nein', + 'additional_software_needed_choice': 'nein', + 'additional_access_needed_choice': 'nein', + 'successor_required_choice': 'nein', + 'inherit_phone_number_choice': 'nein', + 'agreement_confirm': 'on', + } + + response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost', HTTP_ACCEPT_LANGUAGE='en') + + self.assertEqual(response.status_code, 302) + obj = OnboardingRequest.objects.get(work_email='max.mustermann@tub.co') + self.assertEqual(obj.preferred_language, 'en') + mock_delay.assert_called_once_with(obj.id) + + @patch('workflows.views.process_onboarding_request.delay') + def test_onboarding_submit_persists_german_language(self, mock_delay): + payload = { + 'first_name': 'Erika', + 'last_name': 'Muster', + 'gender': 'frau', + 'job_title': 'Consultant', + 'department': 'IT-Service', + 'work_email': 'erika.muster@tub.co', + 'contract_start': '2026-11-02', + 'employment_type': 'unbefristet', + 'group_mailboxes_required_choice': 'nein', + 'additional_hardware_needed_choice': 'nein', + 'additional_software_needed_choice': 'nein', + 'additional_access_needed_choice': 'nein', + 'successor_required_choice': 'nein', + 'inherit_phone_number_choice': 'nein', + 'agreement_confirm': 'on', + } + + response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost', HTTP_ACCEPT_LANGUAGE='de') + + self.assertEqual(response.status_code, 302) + obj = OnboardingRequest.objects.get(work_email='erika.muster@tub.co') + self.assertEqual(obj.preferred_language, 'de') + mock_delay.assert_called_once_with(obj.id) + + @patch('workflows.views.process_offboarding_request.delay') + def test_offboarding_submit_persists_english_language(self, mock_delay): + payload = { + 'full_name': self.profile.full_name, + 'work_email': self.profile.work_email, + 'department': self.profile.department, + 'job_title': self.profile.job_title, + 'last_working_day': '2026-12-31', + 'notes': 'Disable accounts.', + } + + response = self.client.post( + f'/offboarding/new/?profile={self.profile.id}', + payload, + HTTP_HOST='localhost', + HTTP_ACCEPT_LANGUAGE='en', + ) + + self.assertEqual(response.status_code, 302) + obj = OffboardingRequest.objects.get(work_email=self.profile.work_email) + self.assertEqual(obj.preferred_language, 'en') + mock_delay.assert_called_once_with(obj.id) + + @patch('workflows.tasks._apply_notification_rules') + @patch('workflows.tasks._schedule_welcome_email') + @patch('workflows.tasks.upload_to_nextcloud') + @patch('workflows.tasks._send_workflow_email') + @patch('workflows.tasks._generate_onboarding_pdf') + def test_onboarding_task_uses_english_template_for_english_request( + self, + mock_generate_pdf, + mock_send_workflow_email, + mock_upload, + mock_schedule, + mock_rules, + ): + pdf_path = Path('/tmp/onoff_test_pdfs/onboarding_letter_English_Person.pdf') + pdf_path.parent.mkdir(parents=True, exist_ok=True) + pdf_path.write_bytes(b'%PDF-1.4\n%test\n') + mock_generate_pdf.return_value = pdf_path + NotificationTemplate.objects.update_or_create( + key='onboarding_it', + defaults={ + 'subject_template': 'DE IT', + 'subject_template_en': 'EN IT', + 'body_template': 'DE body', + 'body_template_en': 'EN body', + 'is_active': True, + }, + ) + request_obj = OnboardingRequest.objects.create( + full_name='English Person', + gender='herr', + job_title='Engineer', + department='IT-Service', + work_email='english.person@tub.co', + contract_start=date(2026, 11, 1), + employment_type='unbefristet', + onboarded_by_email='requester@tub.co', + onboarded_by_name='Mia Beispiel', + agreement='accepted', + preferred_language='en', + ) + + process_onboarding_request(request_obj.id) + + first_call = mock_send_workflow_email.call_args_list[0].kwargs + self.assertEqual(first_call['subject'], 'EN IT') + self.assertEqual(first_call['body'], 'EN body') + + @patch('workflows.tasks._send_workflow_email') + def test_welcome_email_uses_english_template_for_english_request(self, mock_send_workflow_email): + NotificationTemplate.objects.update_or_create( + key='onboarding_welcome', + defaults={ + 'subject_template': 'DE Welcome', + 'subject_template_en': 'EN Welcome', + 'body_template': 'DE Welcome Body', + 'body_template_en': 'EN Welcome Body', + 'is_active': True, + }, + ) + onboarding = OnboardingRequest.objects.create( + full_name='Welcome Person', + gender='frau', + job_title='Manager', + department='IT-Service', + work_email='welcome.person@tub.co', + contract_start=date(2026, 11, 1), + employment_type='unbefristet', + onboarded_by_email='requester@tub.co', + agreement='accepted', + preferred_language='en', + ) + scheduled = ScheduledWelcomeEmail.objects.create( + onboarding_request=onboarding, + recipient_email='welcome.person@tub.co', + send_at=timezone.now(), + status='scheduled', + ) + + send_scheduled_welcome_email(scheduled.id, True) + + kwargs = mock_send_workflow_email.call_args.kwargs + self.assertEqual(kwargs['subject'], 'EN Welcome') + self.assertEqual(kwargs['body'], 'EN Welcome Body') diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 71417e9..4f25449 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -399,7 +399,7 @@ def onboarding_create(request): if form.is_valid(): obj = form.save() obj.onboarded_by_name = _display_user_name(request.user) - obj.preferred_language = (get_language() or 'de').split('-')[0] + obj.preferred_language = ((getattr(request, 'LANGUAGE_CODE', '') or get_language() or 'de').split('-')[0]) obj.save(update_fields=['onboarded_by_name', 'preferred_language']) process_onboarding_request.delay(obj.id) return redirect(f"/onboarding/new/?saved=1&id={obj.id}") @@ -572,7 +572,7 @@ def offboarding_create(request): else: obj.requested_by_email = settings.DEFAULT_FROM_EMAIL obj.requested_by_name = _display_user_name(request.user) - obj.preferred_language = (get_language() or 'de').split('-')[0] + obj.preferred_language = ((getattr(request, 'LANGUAGE_CODE', '') or get_language() or 'de').split('-')[0]) obj.save() process_offboarding_request.delay(obj.id) return redirect(f"/offboarding/new/?saved=1&id={obj.id}")