snapshot: preserve custom field parity across forms timeline and pdf
This commit is contained in:
@@ -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)
|
||||
|
||||
123
backend/workflows/tests/test_observability_ui.py
Normal file
123
backend/workflows/tests/test_observability_ui.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
73
backend/workflows/tests/test_request_timeline.py
Normal file
73
backend/workflows/tests/test_request_timeline.py
Normal 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.')
|
||||
Reference in New Issue
Block a user