snapshot: preserve request status retry and i18n labels
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2026-03-25 19:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('workflows', '0032_adminauditlog'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='offboardingrequest',
|
||||||
|
name='last_error',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='offboardingrequest',
|
||||||
|
name='processing_status',
|
||||||
|
field=models.CharField(choices=[('submitted', 'Eingereicht'), ('processing', 'In Bearbeitung'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen')], default='submitted', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='onboardingrequest',
|
||||||
|
name='last_error',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='onboardingrequest',
|
||||||
|
name='processing_status',
|
||||||
|
field=models.CharField(choices=[('submitted', 'Eingereicht'), ('processing', 'In Bearbeitung'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen')], default='submitted', max_length=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
def _normalized_language_code(value: str | None) -> str:
|
def _normalized_language_code(value: str | None) -> str:
|
||||||
@@ -50,6 +51,13 @@ class AdminAuditLog(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class OnboardingRequest(models.Model):
|
class OnboardingRequest(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('submitted', _('Eingereicht')),
|
||||||
|
('processing', _('In Bearbeitung')),
|
||||||
|
('completed', _('Abgeschlossen')),
|
||||||
|
('failed', _('Fehlgeschlagen')),
|
||||||
|
]
|
||||||
|
|
||||||
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
||||||
gender = models.CharField(
|
gender = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
@@ -112,6 +120,8 @@ class OnboardingRequest(models.Model):
|
|||||||
|
|
||||||
generated_pdf_path = models.CharField(max_length=500, blank=True)
|
generated_pdf_path = models.CharField(max_length=500, blank=True)
|
||||||
intro_pdf_path = models.CharField(max_length=500, blank=True)
|
intro_pdf_path = models.CharField(max_length=500, blank=True)
|
||||||
|
processing_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted')
|
||||||
|
last_error = models.TextField(blank=True)
|
||||||
preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
|
preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@@ -447,6 +457,8 @@ class SystemEmailConfig(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class OffboardingRequest(models.Model):
|
class OffboardingRequest(models.Model):
|
||||||
|
STATUS_CHOICES = OnboardingRequest.STATUS_CHOICES
|
||||||
|
|
||||||
employee_profile = models.ForeignKey(EmployeeProfile, null=True, blank=True, on_delete=models.SET_NULL)
|
employee_profile = models.ForeignKey(EmployeeProfile, null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
||||||
work_email = models.EmailField(verbose_name='Dienstliche E-Mail-Adresse')
|
work_email = models.EmailField(verbose_name='Dienstliche E-Mail-Adresse')
|
||||||
@@ -460,6 +472,8 @@ class OffboardingRequest(models.Model):
|
|||||||
requested_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person')
|
requested_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person')
|
||||||
preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
|
preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
|
||||||
generated_pdf_path = models.CharField(max_length=500, blank=True)
|
generated_pdf_path = models.CharField(max_length=500, blank=True)
|
||||||
|
processing_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted')
|
||||||
|
last_error = models.TextField(blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
@@ -1195,159 +1195,183 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path:
|
|||||||
@shared_task
|
@shared_task
|
||||||
def process_onboarding_request(onboarding_request_id: int) -> None:
|
def process_onboarding_request(onboarding_request_id: int) -> None:
|
||||||
request_obj = OnboardingRequest.objects.get(id=onboarding_request_id)
|
request_obj = OnboardingRequest.objects.get(id=onboarding_request_id)
|
||||||
it_email, general_info_email, business_card_email, hr_works_email, key_email = _resolve_workflow_emails()
|
request_obj.processing_status = 'processing'
|
||||||
salutation = (request_obj.get_gender_display() or '').strip()
|
request_obj.last_error = ''
|
||||||
display_name = f"{salutation} {request_obj.full_name}".strip()
|
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
try:
|
||||||
|
it_email, general_info_email, business_card_email, hr_works_email, key_email = _resolve_workflow_emails()
|
||||||
|
salutation = (request_obj.get_gender_display() or '').strip()
|
||||||
|
display_name = f"{salutation} {request_obj.full_name}".strip()
|
||||||
|
|
||||||
first_name, last_name = _split_name(request_obj.full_name)
|
first_name, last_name = _split_name(request_obj.full_name)
|
||||||
EmployeeProfile.objects.update_or_create(
|
EmployeeProfile.objects.update_or_create(
|
||||||
work_email=request_obj.work_email,
|
work_email=request_obj.work_email,
|
||||||
defaults={
|
defaults={
|
||||||
'full_name': request_obj.full_name,
|
'full_name': request_obj.full_name,
|
||||||
'first_name': first_name,
|
'first_name': first_name,
|
||||||
'last_name': last_name,
|
'last_name': last_name,
|
||||||
'department': request_obj.department,
|
'department': request_obj.department,
|
||||||
'job_title': request_obj.job_title,
|
'job_title': request_obj.job_title,
|
||||||
},
|
},
|
||||||
)
|
|
||||||
|
|
||||||
pdf_path = _generate_onboarding_pdf(request_obj)
|
|
||||||
request_obj.generated_pdf_path = str(pdf_path)
|
|
||||||
request_obj.save(update_fields=['generated_pdf_path'])
|
|
||||||
|
|
||||||
email_context = {
|
|
||||||
'FULL_NAME': display_name,
|
|
||||||
'VORNAME': first_name,
|
|
||||||
'NACHNAME': last_name,
|
|
||||||
'DEPARTMENT': request_obj.department or '-',
|
|
||||||
'CONTRACT_START': request_obj.contract_start,
|
|
||||||
'EMAIL': request_obj.work_email,
|
|
||||||
'REQUESTED_BY': request_obj.onboarded_by_email or '-',
|
|
||||||
'BUSINESS_CARD_NAME': request_obj.business_card_name or display_name,
|
|
||||||
'BUSINESS_CARD_TITLE': request_obj.business_card_title or '-',
|
|
||||||
'BUSINESS_CARD_EMAIL': request_obj.business_card_email or request_obj.work_email,
|
|
||||||
'BUSINESS_CARD_PHONE': request_obj.business_card_phone or '-',
|
|
||||||
'PDF_LINK': settings.ONBOARDING_SHARED_PDF_LINK,
|
|
||||||
}
|
|
||||||
|
|
||||||
_send_templated_email(
|
|
||||||
template_key='onboarding_it',
|
|
||||||
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:
|
|
||||||
_send_templated_email(
|
|
||||||
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:
|
pdf_path = _generate_onboarding_pdf(request_obj)
|
||||||
_send_templated_email(
|
request_obj.generated_pdf_path = str(pdf_path)
|
||||||
template_key='onboarding_hr_works',
|
request_obj.save(update_fields=['generated_pdf_path'])
|
||||||
context=email_context,
|
|
||||||
to=[hr_works_email],
|
|
||||||
language_code=request_obj.preferred_language,
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'Schlüssel' in request_obj.needed_devices:
|
email_context = {
|
||||||
_send_templated_email(
|
'FULL_NAME': display_name,
|
||||||
template_key='onboarding_key',
|
'VORNAME': first_name,
|
||||||
context=email_context,
|
'NACHNAME': last_name,
|
||||||
to=[key_email],
|
'DEPARTMENT': request_obj.department or '-',
|
||||||
language_code=request_obj.preferred_language,
|
'CONTRACT_START': request_obj.contract_start,
|
||||||
)
|
'EMAIL': request_obj.work_email,
|
||||||
|
'REQUESTED_BY': request_obj.onboarded_by_email or '-',
|
||||||
|
'BUSINESS_CARD_NAME': request_obj.business_card_name or display_name,
|
||||||
|
'BUSINESS_CARD_TITLE': request_obj.business_card_title or '-',
|
||||||
|
'BUSINESS_CARD_EMAIL': request_obj.business_card_email or request_obj.work_email,
|
||||||
|
'BUSINESS_CARD_PHONE': request_obj.business_card_phone or '-',
|
||||||
|
'PDF_LINK': settings.ONBOARDING_SHARED_PDF_LINK,
|
||||||
|
}
|
||||||
|
|
||||||
if request_obj.onboarded_by_email:
|
|
||||||
_send_templated_email(
|
_send_templated_email(
|
||||||
template_key='onboarding_reference',
|
template_key='onboarding_it',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[request_obj.onboarded_by_email],
|
to=[it_email],
|
||||||
attachments=[pdf_path],
|
attachments=[pdf_path],
|
||||||
language_code=request_obj.preferred_language,
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
_apply_notification_rules(
|
if request_obj.order_business_cards:
|
||||||
event_type='onboarding',
|
_send_templated_email(
|
||||||
request_obj=request_obj,
|
template_key='onboarding_business_card',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
pdf_path=pdf_path,
|
to=[business_card_email],
|
||||||
)
|
language_code=request_obj.preferred_language,
|
||||||
|
)
|
||||||
|
|
||||||
_schedule_welcome_email(request_obj)
|
if 'HR Works' in request_obj.needed_accesses:
|
||||||
|
_send_templated_email(
|
||||||
|
template_key='onboarding_hr_works',
|
||||||
|
context=email_context,
|
||||||
|
to=[hr_works_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
|
)
|
||||||
|
|
||||||
upload_to_nextcloud(pdf_path, Path(pdf_path).name)
|
if 'Schlüssel' in request_obj.needed_devices:
|
||||||
|
_send_templated_email(
|
||||||
|
template_key='onboarding_key',
|
||||||
|
context=email_context,
|
||||||
|
to=[key_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
|
)
|
||||||
|
|
||||||
|
if request_obj.onboarded_by_email:
|
||||||
|
_send_templated_email(
|
||||||
|
template_key='onboarding_reference',
|
||||||
|
context=email_context,
|
||||||
|
to=[request_obj.onboarded_by_email],
|
||||||
|
attachments=[pdf_path],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
|
)
|
||||||
|
|
||||||
|
_apply_notification_rules(
|
||||||
|
event_type='onboarding',
|
||||||
|
request_obj=request_obj,
|
||||||
|
context=email_context,
|
||||||
|
pdf_path=pdf_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
_schedule_welcome_email(request_obj)
|
||||||
|
|
||||||
|
upload_to_nextcloud(pdf_path, Path(pdf_path).name)
|
||||||
|
request_obj.processing_status = 'completed'
|
||||||
|
request_obj.last_error = ''
|
||||||
|
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
except Exception as exc:
|
||||||
|
request_obj.processing_status = 'failed'
|
||||||
|
request_obj.last_error = str(exc)
|
||||||
|
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def process_offboarding_request(offboarding_request_id: int) -> None:
|
def process_offboarding_request(offboarding_request_id: int) -> None:
|
||||||
request_obj = OffboardingRequest.objects.get(id=offboarding_request_id)
|
request_obj = OffboardingRequest.objects.get(id=offboarding_request_id)
|
||||||
it_email, general_info_email, _, hr_works_email, _ = _resolve_workflow_emails()
|
request_obj.processing_status = 'processing'
|
||||||
|
request_obj.last_error = ''
|
||||||
|
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
try:
|
||||||
|
it_email, general_info_email, _, hr_works_email, _ = _resolve_workflow_emails()
|
||||||
|
|
||||||
pdf_path = _generate_offboarding_pdf(request_obj)
|
pdf_path = _generate_offboarding_pdf(request_obj)
|
||||||
request_obj.generated_pdf_path = str(pdf_path)
|
request_obj.generated_pdf_path = str(pdf_path)
|
||||||
request_obj.save(update_fields=['generated_pdf_path'])
|
request_obj.save(update_fields=['generated_pdf_path'])
|
||||||
|
|
||||||
email_context = {
|
email_context = {
|
||||||
'FULL_NAME': request_obj.full_name,
|
'FULL_NAME': request_obj.full_name,
|
||||||
'DEPARTMENT': request_obj.department or '-',
|
'DEPARTMENT': request_obj.department or '-',
|
||||||
'LAST_WORKING_DAY': request_obj.last_working_day,
|
'LAST_WORKING_DAY': request_obj.last_working_day,
|
||||||
'REQUESTED_BY': request_obj.requested_by_email,
|
'REQUESTED_BY': request_obj.requested_by_email,
|
||||||
'EMAIL': request_obj.work_email,
|
'EMAIL': request_obj.work_email,
|
||||||
}
|
}
|
||||||
|
|
||||||
_send_templated_email(
|
|
||||||
template_key='offboarding_it',
|
|
||||||
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(
|
|
||||||
work_email=request_obj.work_email,
|
|
||||||
needed_accesses__icontains='HR Works',
|
|
||||||
).exists()
|
|
||||||
if had_hr_works:
|
|
||||||
_send_templated_email(
|
_send_templated_email(
|
||||||
template_key='offboarding_hr_works_disable',
|
template_key='offboarding_it',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[hr_works_email],
|
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,
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
_send_templated_email(
|
had_hr_works = OnboardingRequest.objects.filter(
|
||||||
template_key='offboarding_reference',
|
work_email=request_obj.work_email,
|
||||||
context=email_context,
|
needed_accesses__icontains='HR Works',
|
||||||
to=[request_obj.requested_by_email],
|
).exists()
|
||||||
attachments=[pdf_path],
|
if had_hr_works:
|
||||||
language_code=request_obj.preferred_language,
|
_send_templated_email(
|
||||||
)
|
template_key='offboarding_hr_works_disable',
|
||||||
|
context=email_context,
|
||||||
|
to=[hr_works_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
|
)
|
||||||
|
|
||||||
_apply_notification_rules(
|
_send_templated_email(
|
||||||
event_type='offboarding',
|
template_key='offboarding_reference',
|
||||||
request_obj=request_obj,
|
context=email_context,
|
||||||
context=email_context,
|
to=[request_obj.requested_by_email],
|
||||||
pdf_path=pdf_path,
|
attachments=[pdf_path],
|
||||||
)
|
language_code=request_obj.preferred_language,
|
||||||
|
)
|
||||||
|
|
||||||
upload_to_nextcloud(pdf_path, Path(pdf_path).name)
|
_apply_notification_rules(
|
||||||
|
event_type='offboarding',
|
||||||
|
request_obj=request_obj,
|
||||||
|
context=email_context,
|
||||||
|
pdf_path=pdf_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
upload_to_nextcloud(pdf_path, Path(pdf_path).name)
|
||||||
|
request_obj.processing_status = 'completed'
|
||||||
|
request_obj.last_error = ''
|
||||||
|
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
except Exception as exc:
|
||||||
|
request_obj.processing_status = 'failed'
|
||||||
|
request_obj.last_error = str(exc)
|
||||||
|
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ docker compose exec -T web python manage.py run_staging_e2e_check</code></pre>
|
|||||||
<li>Use real PDF generation tests when changing PDF templates or intro/offboarding document logic.</li>
|
<li>Use real PDF generation tests when changing PDF templates or intro/offboarding document logic.</li>
|
||||||
<li>Use the dedicated Release Checklist page as the final go/no-go runbook before shipping changes.</li>
|
<li>Use the dedicated Release Checklist page as the final go/no-go runbook before shipping changes.</li>
|
||||||
<li>The automated bilingual smoke tests now cover DE/EN request language capture and English email-template rendering.</li>
|
<li>The automated bilingual smoke tests now cover DE/EN request language capture and English email-template rendering.</li>
|
||||||
|
<li>Onboarding and offboarding request objects now expose explicit processing state and last-error fields. Async tasks are responsible for transitioning <code>submitted → processing → completed/failed</code>.</li>
|
||||||
|
<li>The Requests Dashboard includes a retry action for failed requests. Retries reset the error text, set the request back to <code>submitted</code>, and enqueue the appropriate Celery task again.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 id="deploy">12) Deployment and Release Checklist</h2>
|
<h2 id="deploy">12) Deployment and Release Checklist</h2>
|
||||||
|
|||||||
@@ -178,6 +178,7 @@
|
|||||||
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>
|
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>
|
||||||
<li><strong>Audit Log:</strong> staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.</li>
|
<li><strong>Audit Log:</strong> staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.</li>
|
||||||
<li><strong>Requests Dashboard:</strong> search records, open PDFs, delete records (single/bulk for staff).</li>
|
<li><strong>Requests Dashboard:</strong> search records, open PDFs, delete records (single/bulk for staff).</li>
|
||||||
|
<li><strong>Request Status & Retry:</strong> onboarding and offboarding requests now carry explicit processing state (<code>submitted</code>, <code>processing</code>, <code>completed</code>, <code>failed</code>). Failed requests expose the last error and can be retried from the dashboard.</li>
|
||||||
<li><strong>Einweisungs- und Übergabeprotokoll:</strong> staff-only <code>PDF erzeugen</code>, <code>Neu erzeugen</code>, and <code>PDF öffnen</code> actions directly on onboarding rows in the Requests Dashboard.</li>
|
<li><strong>Einweisungs- und Übergabeprotokoll:</strong> staff-only <code>PDF erzeugen</code>, <code>Neu erzeugen</code>, and <code>PDF öffnen</code> actions directly on onboarding rows in the Requests Dashboard.</li>
|
||||||
<li><strong>Einweisung durchführen:</strong> staff-only live checklist page opened from onboarding rows, with draft/completed status, notes, progress tracking, and a separate live-status PDF export.</li>
|
<li><strong>Einweisung durchführen:</strong> staff-only live checklist page opened from onboarding rows, with draft/completed status, notes, progress tracking, and a separate live-status PDF export.</li>
|
||||||
<li><strong>Project Wiki:</strong> this documentation page.</li>
|
<li><strong>Project Wiki:</strong> this documentation page.</li>
|
||||||
|
|||||||
@@ -189,6 +189,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="person-meta">{% trans "Noch nicht verfügbar" %}</span>
|
<span class="person-meta">{% trans "Noch nicht verfügbar" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="person-meta" style="margin-top:8px;">{{ row.status }}</div>
|
||||||
|
{% if row.status_key == 'failed' and row.last_error %}
|
||||||
|
<div class="person-meta" style="margin-top:6px; color:#8e1e1e;">{{ row.last_error|truncatechars:140 }}</div>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<td class="actions-cell intro-panel">
|
<td class="actions-cell intro-panel">
|
||||||
@@ -232,6 +236,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
|
{% if row.status_key == 'failed' %}
|
||||||
|
<form method="post" action="/requests/retry/{{ row.kind_slug }}/{{ row.id }}/" class="inline-delete" onsubmit="return confirm('Eintrag erneut verarbeiten?');">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="btn btn-secondary" type="submit">{% trans "Erneut versuchen" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<form method="post" action="/requests/" class="inline-delete" onsubmit="return confirm('Eintrag wirklich löschen?');">
|
<form method="post" action="/requests/" class="inline-delete" onsubmit="return confirm('Eintrag wirklich löschen?');">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn btn-secondary" type="submit" name="single_delete" value="{{ row.kind_slug }}:{{ row.id }}">{% trans "Löschen" %}</button>
|
<button class="btn btn-secondary" type="submit" name="single_delete" value="{{ row.kind_slug }}:{{ row.id }}">{% trans "Löschen" %}</button>
|
||||||
|
|||||||
@@ -39,4 +39,5 @@ urlpatterns = [
|
|||||||
path('requests/onboarding/<int:request_id>/intro-session/pdf/', views.generate_onboarding_intro_session_pdf, name='generate_onboarding_intro_session_pdf'),
|
path('requests/onboarding/<int:request_id>/intro-session/pdf/', views.generate_onboarding_intro_session_pdf, name='generate_onboarding_intro_session_pdf'),
|
||||||
path('requests/onboarding/<int:request_id>/intro-pdf/generate/', views.generate_onboarding_intro_pdf, name='generate_onboarding_intro_pdf'),
|
path('requests/onboarding/<int:request_id>/intro-pdf/generate/', views.generate_onboarding_intro_pdf, name='generate_onboarding_intro_pdf'),
|
||||||
path('requests/delete/<str:kind>/<int:request_id>/', views.delete_request_from_dashboard, name='delete_request_from_dashboard'),
|
path('requests/delete/<str:kind>/<int:request_id>/', views.delete_request_from_dashboard, name='delete_request_from_dashboard'),
|
||||||
|
path('requests/retry/<str:kind>/<int:request_id>/', views.retry_request_from_dashboard, name='retry_request_from_dashboard'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -147,6 +147,16 @@ def _form_field_labels(form_type: str) -> dict[str, str]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _request_status_label(status_key: str) -> str:
|
||||||
|
labels = {
|
||||||
|
'submitted': _('Eingereicht'),
|
||||||
|
'processing': _('In Bearbeitung'),
|
||||||
|
'completed': _('Abgeschlossen'),
|
||||||
|
'failed': _('Fehlgeschlagen'),
|
||||||
|
}
|
||||||
|
return labels.get(status_key, status_key)
|
||||||
|
|
||||||
|
|
||||||
def _translate_choice_list(choices):
|
def _translate_choice_list(choices):
|
||||||
return [(value, str(label)) for value, label in choices]
|
return [(value, str(label)) for value, label in choices]
|
||||||
|
|
||||||
@@ -383,7 +393,9 @@ def requests_dashboard(request):
|
|||||||
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
||||||
'intro_pdf_url': f"/media/pdfs/{Path(obj.intro_pdf_path).name}" if obj.intro_pdf_path else None,
|
'intro_pdf_url': f"/media/pdfs/{Path(obj.intro_pdf_path).name}" if obj.intro_pdf_path else None,
|
||||||
'intro_session': intro_session,
|
'intro_session': intro_session,
|
||||||
'status': 'PDF erstellt' if obj.generated_pdf_path else 'In Bearbeitung',
|
'status': _request_status_label(obj.processing_status),
|
||||||
|
'status_key': obj.processing_status,
|
||||||
|
'last_error': obj.last_error,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for obj in offboarding_items:
|
for obj in offboarding_items:
|
||||||
@@ -398,7 +410,9 @@ def requests_dashboard(request):
|
|||||||
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
||||||
'intro_pdf_url': None,
|
'intro_pdf_url': None,
|
||||||
'intro_session': None,
|
'intro_session': None,
|
||||||
'status': 'PDF erstellt' if obj.generated_pdf_path else 'In Bearbeitung',
|
'status': _request_status_label(obj.processing_status),
|
||||||
|
'status_key': obj.processing_status,
|
||||||
|
'last_error': obj.last_error,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1682,3 +1696,29 @@ def delete_request_from_dashboard(request, kind: str, request_id: int):
|
|||||||
_audit(request, 'request_deleted', target_type=kind, target_id=request_id, target_label=str(obj))
|
_audit(request, 'request_deleted', target_type=kind, target_id=request_id, target_label=str(obj))
|
||||||
messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde gelöscht.')
|
messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde gelöscht.')
|
||||||
return redirect('requests_dashboard')
|
return redirect('requests_dashboard')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@user_passes_test(_is_staff)
|
||||||
|
@require_POST
|
||||||
|
def retry_request_from_dashboard(request, kind: str, request_id: int):
|
||||||
|
if kind == 'onboarding':
|
||||||
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
||||||
|
obj.processing_status = 'submitted'
|
||||||
|
obj.last_error = ''
|
||||||
|
obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
process_onboarding_request.delay(obj.id)
|
||||||
|
_audit(request, 'request_retried', target_type='onboarding', target_id=obj.id, target_label=obj.full_name)
|
||||||
|
elif kind == 'offboarding':
|
||||||
|
obj = get_object_or_404(OffboardingRequest, id=request_id)
|
||||||
|
obj.processing_status = 'submitted'
|
||||||
|
obj.last_error = ''
|
||||||
|
obj.save(update_fields=['processing_status', 'last_error'])
|
||||||
|
process_offboarding_request.delay(obj.id)
|
||||||
|
_audit(request, 'request_retried', target_type='offboarding', target_id=obj.id, target_label=obj.full_name)
|
||||||
|
else:
|
||||||
|
messages.error(request, f'Unbekannter Typ: {kind}')
|
||||||
|
return redirect('requests_dashboard')
|
||||||
|
|
||||||
|
messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde erneut angestoßen.')
|
||||||
|
return redirect('requests_dashboard')
|
||||||
|
|||||||
Reference in New Issue
Block a user