snapshot: preserve custom field parity across forms timeline and pdf

This commit is contained in:
Md Bayazid Bostame
2026-03-27 13:21:25 +01:00
parent 2e5e941d41
commit fdc27f2123
20 changed files with 2294 additions and 545 deletions

View File

@@ -3,7 +3,7 @@ import json
from django.contrib.auth import get_user_model
from django.test import TestCase
from workflows.models import FormConditionalRuleConfig, FormFieldConfig, FormOption, FormSectionConfig
from workflows.models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormOption, FormSectionConfig
class FormBuilderAdminTests(TestCase):
@@ -202,3 +202,59 @@ class FormBuilderAdminTests(TestCase):
self.assertEqual(len(rule.clauses), 2)
self.assertEqual(rule.clauses[0]['field'], 'successor_required_choice')
self.assertEqual(rule.clauses[1]['operator'], 'not_equals')
def test_staff_can_add_custom_field(self):
self.client.force_login(self.staff)
response = self.client.post(
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
data={
'builder_action': 'add_custom_field',
'custom_label': 'Laptop-Tag',
'custom_label_en': 'Laptop tag',
'custom_section_key': 'itsetup',
'custom_field_type': 'text',
'custom_sort_order': '3',
'custom_is_required': 'on',
},
HTTP_HOST='localhost',
)
self.assertEqual(response.status_code, 302)
field = FormCustomFieldConfig.objects.get(form_type='onboarding', field_key='laptop_tag')
self.assertEqual(field.section_key, 'itsetup')
self.assertEqual(field.field_type, 'text')
self.assertEqual(field.is_required, True)
def test_save_order_updates_custom_field_section_and_sort_order(self):
self.client.force_login(self.staff)
custom_field = FormCustomFieldConfig.objects.create(
form_type='onboarding',
field_key='laptop_tag',
section_key='itsetup',
sort_order=99,
field_type='text',
label='Laptop-Tag',
)
self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost')
payload = {
'form_type': 'onboarding',
'columns': {
'stammdaten': ['department'],
'vertrag': ['contract_start'],
'itsetup': ['custom__laptop_tag'],
'abschluss': [],
},
}
response = self.client.post(
'/admin-tools/form-builder/save-order/',
data=json.dumps(payload),
content_type='application/json',
HTTP_HOST='localhost',
)
self.assertEqual(response.status_code, 200)
custom_field.refresh_from_db()
self.assertEqual(custom_field.section_key, 'itsetup')
self.assertEqual(custom_field.sort_order, 2)

View File

@@ -0,0 +1,123 @@
from datetime import timedelta
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.utils import timezone
from workflows.models import AsyncTaskLog
from workflows.roles import ROLE_ADMIN, ROLE_STAFF, assign_user_role
class ObservabilityUITests(TestCase):
def setUp(self):
user_model = get_user_model()
self.admin = user_model.objects.create_user(
username='ops_admin',
email='ops-admin@example.com',
password='secret123',
)
assign_user_role(self.admin, ROLE_ADMIN)
self.staff = user_model.objects.create_user(
username='ops_staff',
email='ops-staff@example.com',
password='secret123',
)
assign_user_role(self.staff, ROLE_STAFF)
def _create_log(self, *, status: str, task_name: str, target_label: str, error_message: str = '') -> AsyncTaskLog:
log = AsyncTaskLog.objects.create(
task_name=task_name,
status=status,
target_type='request',
target_id=1,
target_label=target_label,
error_message=error_message,
)
AsyncTaskLog.objects.filter(id=log.id).update(
started_at=timezone.now() - timedelta(hours=2),
finished_at=timezone.now() - timedelta(hours=1, minutes=45),
)
return AsyncTaskLog.objects.get(id=log.id)
def test_home_shows_operations_overview_for_admin(self):
self._create_log(
status='failed',
task_name='send_scheduled_welcome_email',
target_label='Request A',
error_message='smtp failed hard',
)
self._create_log(
status='succeeded',
task_name='process_onboarding_request',
target_label='Request B',
)
self._create_log(
status='started',
task_name='process_offboarding_request',
target_label='Request C',
)
client = Client()
client.force_login(self.admin)
response = client.get('/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Operations Overview')
self.assertContains(response, 'Fehlgeschlagene Jobs (24h)')
self.assertContains(response, '<strong class="is-error">1</strong>', html=True)
self.assertContains(response, 'send_scheduled_welcome_email')
self.assertContains(response, 'Backup-Status')
def test_home_hides_operations_overview_for_staff(self):
self._create_log(
status='failed',
task_name='process_onboarding_request',
target_label='Request A',
error_message='pdf failed',
)
client = Client()
client.force_login(self.staff)
response = client.get('/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'Operations Overview')
self.assertNotContains(response, 'Job Monitor öffnen')
def test_job_monitor_summary_shows_recent_counts(self):
self._create_log(
status='failed',
task_name='process_onboarding_request',
target_label='Request A',
error_message='pdf failed',
)
self._create_log(
status='succeeded',
task_name='process_offboarding_request',
target_label='Request B',
)
self._create_log(
status='started',
task_name='send_scheduled_welcome_email',
target_label='Request C',
)
client = Client()
client.force_login(self.admin)
response = client.get('/admin-tools/jobs/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Fehlgeschlagene Jobs (24h)')
self.assertContains(response, 'Erfolgreiche Jobs (24h)')
self.assertContains(response, 'Offene Starts (24h)')
self.assertContains(response, 'Zuletzt fehlgeschlagen')
self.assertContains(response, 'pdf failed')
def test_job_monitor_requires_capability(self):
client = Client()
client.force_login(self.staff)
response = client.get('/admin-tools/jobs/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 302)

View File

@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
from django.test import TestCase
from workflows.branding import get_company_email_domain
from workflows.models import EmployeeProfile, OffboardingRequest
from workflows.models import EmployeeProfile, FormCustomFieldConfig, OffboardingRequest
class OffboardingFlowTests(TestCase):
@@ -59,3 +59,37 @@ class OffboardingFlowTests(TestCase):
self.assertEqual(obj.requested_by_email, f'operator@{self.company_domain}')
self.assertEqual(obj.requested_by_name, 'Nina Admin')
mock_delay.assert_called_once_with(obj.id)
@patch('workflows.views.process_offboarding_request.delay')
def test_offboarding_custom_field_is_saved(self, mock_delay):
FormCustomFieldConfig.objects.create(
form_type='offboarding',
field_key='return_comment',
section_key='abschluss',
sort_order=0,
field_type='textarea',
is_active=True,
is_required=False,
label='Rückgabehinweis',
)
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': 'Bitte Accounts sperren.',
'custom__return_comment': 'Abholung durch IT am Freitag.',
}
response = self.client.post(
f'/offboarding/new/?profile={self.profile.id}',
payload,
HTTP_HOST='localhost',
)
self.assertEqual(response.status_code, 302)
obj = OffboardingRequest.objects.get(work_email=self.profile.work_email)
self.assertEqual(obj.custom_field_values, {'return_comment': 'Abholung durch IT am Freitag.'})
mock_delay.assert_called_once_with(obj.id)

View File

@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
from django.test import TestCase
from workflows.branding import get_company_email_domain
from workflows.models import FormConditionalRuleConfig, FormFieldConfig, FormSectionConfig, OnboardingRequest
from workflows.models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormSectionConfig, OnboardingRequest
class OnboardingFlowTests(TestCase):
@@ -171,3 +171,168 @@ class OnboardingFlowTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertIn('employment-end-box', html)
self.assertIn('"value": "unbefristet"', html)
def test_onboarding_custom_field_uses_combined_order(self):
FormCustomFieldConfig.objects.create(
form_type='onboarding',
field_key='office_location',
section_key='stammdaten',
sort_order=1,
field_type='text',
is_active=True,
label='Bürostandort',
)
FormFieldConfig.objects.update_or_create(
form_type='onboarding',
field_name='gender',
defaults={'sort_order': 2, 'page_key': 'stammdaten'},
)
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
html = response.content.decode('utf-8')
self.assertLess(html.index('Bürostandort'), html.index('Anrede'))
@patch('workflows.views.process_onboarding_request.delay')
def test_onboarding_custom_field_is_rendered_and_saved(self, mock_delay):
FormCustomFieldConfig.objects.create(
form_type='onboarding',
field_key='office_location',
section_key='stammdaten',
sort_order=0,
field_type='text',
is_active=True,
is_required=True,
label='Bürostandort',
)
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
self.assertContains(response, 'Bürostandort')
payload = {
'first_name': 'Mara',
'last_name': 'Muster',
'gender': 'frau',
'job_title': 'Consultant',
'department': 'IT-Service',
'work_email': f'mara.muster@{self.company_domain}',
'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',
'custom__office_location': 'Berlin Mitte',
'agreement_confirm': 'on',
}
submit_response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost')
self.assertEqual(submit_response.status_code, 302)
obj = OnboardingRequest.objects.get(work_email=f'mara.muster@{self.company_domain}')
self.assertEqual(obj.custom_field_values, {'office_location': 'Berlin Mitte'})
mock_delay.assert_called_once_with(obj.id)
@patch('workflows.views.process_onboarding_request.delay')
def test_hidden_required_custom_field_does_not_block_submission(self, mock_delay):
FormCustomFieldConfig.objects.create(
form_type='onboarding',
field_key='visitor_badge_name',
section_key='stammdaten',
sort_order=0,
field_type='text',
is_active=True,
is_required=True,
label='Besucherausweis',
)
FormConditionalRuleConfig.objects.update_or_create(
form_type='onboarding',
target_key='custom__visitor_badge_name',
defaults={
'is_active': True,
'clauses': [{'field': 'order_business_cards', 'operator': 'checked', 'value': True}],
},
)
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
html = response.content.decode('utf-8')
self.assertIn('custom__visitor_badge_name', html)
self.assertIn('"custom__visitor_badge_name"', html)
payload = {
'first_name': 'Lea',
'last_name': 'Leicht',
'gender': 'frau',
'job_title': 'Consultant',
'department': 'IT-Service',
'work_email': f'lea.leicht@{self.company_domain}',
'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',
}
submit_response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost')
self.assertEqual(submit_response.status_code, 302)
obj = OnboardingRequest.objects.get(work_email=f'lea.leicht@{self.company_domain}')
self.assertEqual(obj.custom_field_values, {'visitor_badge_name': ''})
mock_delay.assert_called_once_with(obj.id)
@patch('workflows.views.process_onboarding_request.delay')
def test_visible_required_custom_field_blocks_submission(self, mock_delay):
FormCustomFieldConfig.objects.create(
form_type='onboarding',
field_key='visitor_badge_name',
section_key='stammdaten',
sort_order=0,
field_type='text',
is_active=True,
is_required=True,
label='Besucherausweis',
)
FormConditionalRuleConfig.objects.update_or_create(
form_type='onboarding',
target_key='custom__visitor_badge_name',
defaults={
'is_active': True,
'clauses': [{'field': 'order_business_cards', 'operator': 'checked', 'value': True}],
},
)
payload = {
'first_name': 'Lia',
'last_name': 'Laut',
'gender': 'frau',
'job_title': 'Consultant',
'department': 'IT-Service',
'work_email': f'lia.laut@{self.company_domain}',
'contract_start': '2026-11-01',
'employment_type': 'unbefristet',
'order_business_cards': 'on',
'business_card_name': 'Lia Laut',
'business_card_title': 'Consultant',
'business_card_email': f'lia.laut@{self.company_domain}',
'business_card_phone': '030 123456',
'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')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Besucherausweis')
self.assertFalse(OnboardingRequest.objects.filter(work_email=f'lia.laut@{self.company_domain}').exists())
mock_delay.assert_not_called()

View File

@@ -1,6 +1,6 @@
from django.test import TestCase
from workflows.models import FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest
from workflows.models import FormCustomFieldConfig, FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest
from workflows.pdf_sections import build_pdf_sections
@@ -94,3 +94,32 @@ class PDFSectionBuilderTests(TestCase):
self.assertIn('last_working_day', [field['name'] for field in austritt['fields']])
date_field = next(field for field in austritt['fields'] if field['name'] == 'last_working_day')
self.assertTrue(date_field['display_value'])
def test_custom_fields_are_included_in_pdf_sections(self):
FormCustomFieldConfig.objects.create(
form_type='onboarding',
field_key='office_location',
section_key='stammdaten',
sort_order=0,
field_type='text',
is_active=True,
label='Bürostandort',
)
request_obj = OnboardingRequest.objects.create(
full_name='Max Mustermann',
gender='herr',
job_title='Consultant',
department='IT-Service',
work_email='max.mustermann@workdock.de',
contract_start='2026-11-01',
employment_type='unbefristet',
agreement='accepted',
custom_field_values={'office_location': 'Berlin Mitte'},
)
sections = build_pdf_sections('onboarding', request_obj, 'de')
stammdaten = next(section for section in sections if section['key'] == 'stammdaten')
custom_field = next(field for field in stammdaten['fields'] if field['name'] == 'custom__office_location')
self.assertEqual(custom_field['label'], 'Bürostandort')
self.assertEqual(custom_field['display_value'], 'Berlin Mitte')

View File

@@ -0,0 +1,73 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from workflows.models import FormCustomFieldConfig, OffboardingRequest, OnboardingRequest
from workflows.roles import ROLE_ADMIN, assign_user_role
class RequestTimelineCustomFieldTests(TestCase):
def setUp(self):
user_model = get_user_model()
self.user = user_model.objects.create_user(
username='timeline_admin',
email='timeline-admin@example.com',
password='secret123',
)
assign_user_role(self.user, ROLE_ADMIN)
self.client.force_login(self.user)
def test_onboarding_timeline_renders_custom_field_values(self):
FormCustomFieldConfig.objects.create(
form_type='onboarding',
field_key='office_location',
section_key='stammdaten',
sort_order=0,
field_type='text',
is_active=True,
label='Bürostandort',
)
obj = OnboardingRequest.objects.create(
full_name='Max Mustermann',
gender='herr',
job_title='Consultant',
department='IT-Service',
work_email='max.mustermann@workdock.de',
contract_start='2026-11-01',
employment_type='unbefristet',
agreement='accepted',
custom_field_values={'office_location': 'Berlin Mitte'},
)
response = self.client.get(f'/requests/timeline/onboarding/{obj.id}/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Benutzerdefinierte Felder')
self.assertContains(response, 'Bürostandort')
self.assertContains(response, 'Berlin Mitte')
def test_offboarding_timeline_renders_custom_field_values(self):
FormCustomFieldConfig.objects.create(
form_type='offboarding',
field_key='return_comment',
section_key='abschluss',
sort_order=0,
field_type='textarea',
is_active=True,
label='Rückgabehinweis',
)
obj = OffboardingRequest.objects.create(
full_name='Lara Beispiel',
work_email='lara.beispiel@workdock.de',
department='IT-Service',
job_title='Engineer',
last_working_day='2026-12-31',
requested_by_email='admin@workdock.de',
custom_field_values={'return_comment': 'Abholung durch IT am Freitag.'},
)
response = self.client.get(f'/requests/timeline/offboarding/{obj.id}/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Benutzerdefinierte Felder')
self.assertContains(response, 'Rückgabehinweis')
self.assertContains(response, 'Abholung durch IT am Freitag.')