2020-11-24 12:44:07 +00:00
|
|
|
import csv
|
|
|
|
|
|
2020-09-21 12:27:16 +00:00
|
|
|
from django.contrib import admin
|
2020-11-24 12:44:07 +00:00
|
|
|
from django.http import HttpResponse
|
2025-09-28 22:25:16 +00:00
|
|
|
from .models import ProjectRequest, ProjectsDeclined
|
|
|
|
|
from .forms import ProjectRequestAdminForm
|
|
|
|
|
from django.db import models
|
|
|
|
|
from django import forms
|
|
|
|
|
from django.contrib import messages
|
|
|
|
|
from .services import approve_project_request, decline_project_request
|
|
|
|
|
from django.contrib.admin.helpers import ActionForm
|
|
|
|
|
|
2020-09-21 12:27:16 +00:00
|
|
|
|
2025-08-20 10:06:43 +00:00
|
|
|
from .models import (
|
|
|
|
|
Account,
|
|
|
|
|
Project,
|
|
|
|
|
HonoraryCertificate,
|
|
|
|
|
Library,
|
|
|
|
|
ELiterature,
|
|
|
|
|
Software,
|
|
|
|
|
IFG,
|
|
|
|
|
Travel,
|
|
|
|
|
Email,
|
|
|
|
|
BusinessCard,
|
|
|
|
|
List,
|
|
|
|
|
Literature,
|
|
|
|
|
)
|
2020-10-06 12:55:46 +00:00
|
|
|
|
2020-11-24 12:44:07 +00:00
|
|
|
|
|
|
|
|
def export_as_csv(self, request, queryset):
|
|
|
|
|
|
|
|
|
|
meta = self.model._meta
|
|
|
|
|
field_names = [field.name for field in meta.fields]
|
|
|
|
|
|
|
|
|
|
response = HttpResponse(content_type='text/csv')
|
|
|
|
|
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
|
|
|
|
|
writer = csv.writer(response)
|
|
|
|
|
|
|
|
|
|
writer.writerow(field_names)
|
|
|
|
|
for obj in queryset:
|
|
|
|
|
row = writer.writerow([getattr(obj, field) for field in field_names])
|
|
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
2023-02-27 17:09:29 +00:00
|
|
|
export_as_csv.short_description = "Ausgewähltes zu CSV exportieren"
|
2020-11-24 12:44:07 +00:00
|
|
|
|
|
|
|
|
admin.site.add_action(export_as_csv)
|
|
|
|
|
|
2020-10-06 11:17:28 +00:00
|
|
|
@admin.register(Project)
|
|
|
|
|
class ProjectAdmin(admin.ModelAdmin):
|
2020-11-19 13:41:31 +00:00
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal')
|
|
|
|
|
list_display = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal')
|
2023-02-27 17:09:29 +00:00
|
|
|
fields = ('realname', 'email', 'granted', 'granted_date', 'mail_state', 'end_mail_send', 'survey_mail_send', 'survey_mail_date', 'name', 'description', 'pid', 'finance_id', 'start', 'end', 'otrs', 'plan', 'page', 'urls', 'group', 'location', 'participants_estimated', 'participants_real', 'insurance', 'insurance_technic', 'support', 'cost', 'account', 'granted_from', 'notes', 'intern_notes', 'status', 'project_of_year', 'end_quartal')
|
2020-11-24 12:44:07 +00:00
|
|
|
# action = ['export_as_csv']
|
2023-02-27 17:09:29 +00:00
|
|
|
date_hierarchy = 'end'
|
2023-02-27 17:09:29 +00:00
|
|
|
readonly_fields = ('end_quartal', 'project_of_year', 'pid', 'finance_id')
|
2023-02-27 17:09:28 +00:00
|
|
|
|
2023-12-30 19:01:06 +00:00
|
|
|
class Media:
|
|
|
|
|
js = ('dropdown/js/otrs_link.js',)
|
|
|
|
|
|
|
|
|
|
|
2023-12-30 17:46:15 +00:00
|
|
|
|
2023-02-27 17:09:28 +00:00
|
|
|
@admin.register(BusinessCard)
|
|
|
|
|
class BusinessCardAdmin(admin.ModelAdmin):
|
2025-08-19 12:10:15 +00:00
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
|
2025-08-26 11:53:39 +00:00
|
|
|
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
|
2023-02-27 17:09:28 +00:00
|
|
|
list_display_links = ('realname', 'service_id')
|
2023-02-27 17:09:28 +00:00
|
|
|
# action = ['export_as_csv']
|
2023-02-27 17:09:29 +00:00
|
|
|
date_hierarchy = 'granted_date'
|
2023-02-27 17:09:29 +00:00
|
|
|
readonly_fields = ['service_id']
|
2023-02-27 17:09:28 +00:00
|
|
|
class Media:
|
|
|
|
|
js = ('dropdown/js/base.js',)
|
|
|
|
|
|
2023-02-27 17:09:28 +00:00
|
|
|
@admin.register(Literature)
|
|
|
|
|
class LiteratureAdmin(admin.ModelAdmin):
|
2023-02-27 17:09:29 +00:00
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
|
2025-08-26 11:53:39 +00:00
|
|
|
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display_links = ('realname', 'service_id')
|
|
|
|
|
date_hierarchy = 'granted_date'
|
2023-02-27 17:09:29 +00:00
|
|
|
readonly_fields = ['service_id']
|
2023-02-27 17:09:28 +00:00
|
|
|
|
2023-02-27 17:09:28 +00:00
|
|
|
|
2023-02-27 17:09:29 +00:00
|
|
|
@admin.register(Account)
|
|
|
|
|
class AccountAdmin(admin.ModelAdmin):
|
|
|
|
|
save_as = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(HonoraryCertificate)
|
|
|
|
|
class HonoraryCertificateAdmin(admin.ModelAdmin):
|
|
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ['realname', 'granted', 'project__name', 'project__pid']
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display = ('realname', 'granted','project')
|
2023-02-27 17:09:29 +00:00
|
|
|
date_hierarchy = 'granted_date'
|
2023-02-27 17:09:29 +00:00
|
|
|
autocomplete_fields = ['project']
|
2025-02-27 09:15:25 +00:00
|
|
|
class Media:
|
|
|
|
|
js = ('dropdown/js/otrs_link.js',)
|
2023-02-27 17:09:29 +00:00
|
|
|
|
2025-08-20 10:06:43 +00:00
|
|
|
|
|
|
|
|
@admin.register(Library, ELiterature, Software)
|
2023-02-27 17:09:29 +00:00
|
|
|
class LibraryAdmin(admin.ModelAdmin):
|
|
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display = ('realname', 'service_id', 'granted', 'granted_date')
|
|
|
|
|
list_display_links = ('realname', 'service_id')
|
|
|
|
|
date_hierarchy = 'granted_date'
|
2023-02-27 17:09:29 +00:00
|
|
|
readonly_fields = ['service_id']
|
2025-08-20 10:06:43 +00:00
|
|
|
exclude = ['type']
|
|
|
|
|
|
|
|
|
|
def get_queryset(self, request):
|
|
|
|
|
return super().get_queryset(request).filter(type=self.model.TYPE)
|
|
|
|
|
|
|
|
|
|
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
|
|
|
if db_field.name == 'library':
|
|
|
|
|
kwargs['label'] = self.model.LIBRARY_LABEL
|
|
|
|
|
kwargs['help_text'] = self.model.LIBRARY_HELP_TEXT
|
|
|
|
|
|
|
|
|
|
elif db_field.name == 'duration':
|
|
|
|
|
kwargs['help_text'] = self.model.DURATION_HELP_TEXT
|
|
|
|
|
|
|
|
|
|
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
|
|
|
|
2023-02-27 17:09:29 +00:00
|
|
|
|
|
|
|
|
@admin.register(IFG)
|
|
|
|
|
class IFGAdmin(admin.ModelAdmin):
|
|
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display = ('realname', 'service_id', 'granted', 'granted_date')
|
|
|
|
|
list_display_links = ('realname', 'service_id')
|
|
|
|
|
date_hierarchy = 'granted_date'
|
2023-02-27 17:09:29 +00:00
|
|
|
readonly_fields = ['service_id']
|
2023-02-27 17:09:29 +00:00
|
|
|
|
|
|
|
|
@admin.register(Travel)
|
|
|
|
|
class TravelAdmin(admin.ModelAdmin):
|
|
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid']
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project', 'project_end_quartal')
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display_links = ('realname', 'project')
|
2023-02-27 17:09:29 +00:00
|
|
|
date_hierarchy = 'project_end'
|
2023-02-27 17:09:29 +00:00
|
|
|
autocomplete_fields = ['project']
|
2025-08-19 12:10:36 +00:00
|
|
|
readonly_fields = ['service_id', 'project_end', 'project_end_quartal']
|
2023-02-27 17:09:29 +00:00
|
|
|
|
2025-02-26 12:13:46 +00:00
|
|
|
class Media:
|
|
|
|
|
js = ('dropdown/js/otrs_link.js',)
|
|
|
|
|
|
|
|
|
|
|
2023-02-27 17:09:29 +00:00
|
|
|
@admin.register(Email)
|
|
|
|
|
class EmailAdmin(admin.ModelAdmin):
|
|
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
|
2025-08-26 11:53:39 +00:00
|
|
|
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display_links = ('realname', 'service_id')
|
|
|
|
|
date_hierarchy = 'granted_date'
|
2023-02-27 17:09:29 +00:00
|
|
|
radio_fields = {'adult': admin.VERTICAL}
|
2023-02-27 17:09:29 +00:00
|
|
|
readonly_fields = ['service_id']
|
2023-02-27 17:09:29 +00:00
|
|
|
class Media:
|
|
|
|
|
js = ('dropdown/js/base.js',)
|
2023-02-27 17:09:29 +00:00
|
|
|
|
2020-10-06 11:17:28 +00:00
|
|
|
|
2023-02-27 17:09:29 +00:00
|
|
|
@admin.register(List)
|
|
|
|
|
class ListAdmin(admin.ModelAdmin):
|
|
|
|
|
save_as = True
|
2023-02-27 17:09:29 +00:00
|
|
|
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
|
2025-08-26 11:53:39 +00:00
|
|
|
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
|
2023-02-27 17:09:29 +00:00
|
|
|
list_display_links = ('realname', 'service_id')
|
|
|
|
|
date_hierarchy = 'granted_date'
|
2023-02-27 17:09:29 +00:00
|
|
|
readonly_fields = ['service_id']
|
2023-02-27 17:09:29 +00:00
|
|
|
|
2025-09-28 22:25:16 +00:00
|
|
|
|
|
|
|
|
class ApproveActionForm(ActionForm):
|
|
|
|
|
"""
|
|
|
|
|
Extra control rendered next to the bulk actions dropdown.
|
|
|
|
|
Admin must choose an Account (Kostenstelle) when approving requests.
|
|
|
|
|
"""
|
|
|
|
|
account_code = forms.ModelChoiceField(queryset=Account.objects.all(), required=True,
|
|
|
|
|
label='Kostenstelle (Account)')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(ProjectRequest)
|
|
|
|
|
class ProjectRequestAdmin(admin.ModelAdmin):
|
|
|
|
|
"""
|
|
|
|
|
Admin for incoming project requests (< 1000 EUR).
|
|
|
|
|
|
|
|
|
|
- Uses a custom ModelForm to render JSON-backed fields (checkbox multiselects).
|
|
|
|
|
- Provides two bulk actions: approve (moves to Projects) and decline (moves to Projects_declined).
|
|
|
|
|
- Enforces that an Account must be selected for approval.
|
|
|
|
|
"""
|
|
|
|
|
form = ProjectRequestAdminForm
|
|
|
|
|
save_as = True
|
|
|
|
|
list_display = ('name', 'realname', 'email', 'start', 'end', 'cost', 'decision')
|
|
|
|
|
list_filter = ('decision', 'insurance',)
|
|
|
|
|
search_fields = ('name', 'realname', 'email')
|
|
|
|
|
readonly_fields = ('decision', 'decision_date', 'decided_by')
|
|
|
|
|
|
|
|
|
|
# Make text areas more comfortable to edit in admin
|
|
|
|
|
formfield_overrides = {
|
|
|
|
|
# Increase rows for TextField edit widgets
|
|
|
|
|
models.TextField: {'widget': forms.Textarea(attrs={'rows': 5, 'cols': 80})},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Show actions at the top (common UX in Django admin)
|
|
|
|
|
actions = ['approve_selected', 'decline_selected']
|
|
|
|
|
actions_on_top = True
|
|
|
|
|
actions_on_bottom = False
|
|
|
|
|
action_form = ApproveActionForm
|
|
|
|
|
|
|
|
|
|
@admin.action(description='Bewilligen → nach „Projects“')
|
|
|
|
|
def approve_selected(self, request, queryset):
|
|
|
|
|
"""
|
|
|
|
|
Bulk-approve selected requests:
|
|
|
|
|
- Requires an Account (Kostenstelle) chosen via ApproveActionForm.
|
|
|
|
|
- Delegates the creation/move logic to the service layer.
|
|
|
|
|
- Counts successes and reports via Django messages.
|
|
|
|
|
"""
|
|
|
|
|
account_pk = request.POST.get('account_code')
|
|
|
|
|
if not account_pk:
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
'Bitte eine Kostenstelle auswählen.',
|
|
|
|
|
level=messages.ERROR
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
account = Account.objects.get(pk=account_pk)
|
|
|
|
|
except Account.DoesNotExist:
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
f'Unbekannte Kostenstelle (Account pk={account_pk}).',
|
|
|
|
|
level=messages.ERROR
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
decided_by = request.user.get_username()
|
|
|
|
|
ok, failed = 0, 0
|
|
|
|
|
|
|
|
|
|
for req in queryset:
|
|
|
|
|
try:
|
|
|
|
|
# Service call is atomic and locks the row (select_for_update)
|
|
|
|
|
approve_project_request(req.id, decided_by, account.code)
|
|
|
|
|
ok += 1
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
failed += 1
|
|
|
|
|
# Show a concise per-object error; keep details in server logs if needed
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
f'Fehler beim Bewilligen von „{req}“: {exc}',
|
|
|
|
|
level=messages.ERROR
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if ok:
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
f'{ok} Antrag/Anträge bewilligt und als Project angelegt.',
|
|
|
|
|
level=messages.SUCCESS
|
|
|
|
|
)
|
|
|
|
|
if failed:
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
f'{failed} Antrag/Anträge konnten nicht bewilligt werden.',
|
|
|
|
|
level=messages.WARNING
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@admin.action(description='Ablehnen → nach „Projects_declined“')
|
|
|
|
|
def decline_selected(self, request, queryset):
|
|
|
|
|
"""
|
|
|
|
|
Bulk-decline selected requests:
|
|
|
|
|
- Archives a minimal snapshot to Projects_declined (per ticket).
|
|
|
|
|
- Delegates the move logic to the service layer.
|
|
|
|
|
"""
|
|
|
|
|
ok, failed = 0, 0
|
|
|
|
|
|
|
|
|
|
for req in queryset:
|
|
|
|
|
try:
|
|
|
|
|
decline_project_request(req.id, reason='')
|
|
|
|
|
ok += 1
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
failed += 1
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
f'Fehler beim Ablehnen von „{req}“: {exc}',
|
|
|
|
|
level=messages.ERROR
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if ok:
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
f'{ok} Antrag/Anträge abgelehnt → „Projects_declined“',
|
|
|
|
|
level=messages.WARNING
|
|
|
|
|
)
|
|
|
|
|
if failed:
|
|
|
|
|
self.message_user(
|
|
|
|
|
request,
|
|
|
|
|
f'{failed} Antrag/Anträge konnten nicht abgelehnt werden.',
|
|
|
|
|
level=messages.ERROR
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(ProjectsDeclined)
|
|
|
|
|
class ProjectsDeclinedAdmin(admin.ModelAdmin):
|
|
|
|
|
"""
|
|
|
|
|
Read-only-ish list of declined requests for auditing.
|
|
|
|
|
"""
|
|
|
|
|
list_display = ('name', 'realname', 'email', 'decision_date')
|
|
|
|
|
search_fields = ('name', 'realname', 'email')
|
|
|
|
|
date_hierarchy = 'decision_date'
|
|
|
|
|
|
2023-02-27 17:09:29 +00:00
|
|
|
# commented out because of the individual registering to control displays in admin panel
|
|
|
|
|
|
|
|
|
|
#admin.site.register([
|
|
|
|
|
# Account,
|
|
|
|
|
# HonoraryCertificate,
|
|
|
|
|
# Library,
|
|
|
|
|
# IFG,
|
|
|
|
|
# Travel,
|
|
|
|
|
# Email,
|
|
|
|
|
# List,
|
|
|
|
|
# ])
|