Skip to content
Snippets Groups Projects
Commit b9357222 authored by Nadja Geisler's avatar Nadja Geisler :sunny:
Browse files

Merge branch 'feature-admin-improvements' into 'main'

Admin interface improvements

Closes #234, #227, #241, #233, #217, and #223

See merge request !242
parents 2b47d89d 53b063c7
Branches
No related tags found
1 merge request!242Admin interface improvements
Pipeline #274513 canceled
......@@ -2,6 +2,8 @@ from django import forms
from django.apps import apps
from django.contrib import admin, messages
from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter, action, display
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User # pylint: disable=E5142
from django.db.models import Count, F
from django.http import HttpResponseRedirect
from django.shortcuts import render, redirect
......@@ -568,3 +570,41 @@ class DefaultSlotAdmin(EventTimezoneFormMixin, admin.ModelAdmin):
list_display = ['start_simplified', 'end_simplified', 'event']
list_filter = ['event']
form = DefaultSlotAdminForm
# Define a new User admin
class UserAdmin(BaseUserAdmin):
"""
Admin interface for Users
Enhances the built-in UserAdmin with additional actions to activate and deactivate users and a custom selection
of displayed properties in overview list
"""
list_display = ["username", "email", "is_active", "is_staff", "is_superuser"]
actions = ['activate', 'deactivate']
@admin.action(description=_("Activate selected users"))
def activate(self, request, queryset):
"""
Bulk activate users
:param request: HTTP request
:param queryset: queryset containing all users that should be activated
"""
queryset.update(is_active=True)
self.message_user(request, _("The selected users have been activated."))
@admin.action(description=_("Deactivate selected users"))
def deactivate(self, request, queryset):
"""
Bulk deactivate users
:param request: HTTP request
:param queryset: queryset containing all users that should be deactivated
"""
queryset.update(is_active=False)
self.message_user(request, _("The selected users have been deactivated."))
# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
......@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-25 22:33+0100\n"
"POT-Creation-Date: 2025-02-26 16:35+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: AKModel/admin.py:86 AKModel/admin.py:96
#: AKModel/admin.py:88 AKModel/admin.py:98
#: AKModel/templates/admin/AKModel/event_wizard/activate.html:35
#: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:51
#: AKModel/templates/admin/AKModel/event_wizard/finish.html:21
......@@ -21,69 +21,85 @@ msgstr ""
msgid "Status"
msgstr "Status"
#: AKModel/admin.py:98
#: AKModel/admin.py:100
msgid "Toggle plan visibility"
msgstr "Plansichtbarkeit ändern"
#: AKModel/admin.py:110 AKModel/admin.py:121 AKModel/views/manage.py:138
#: AKModel/admin.py:112 AKModel/admin.py:123 AKModel/views/manage.py:138
msgid "Publish plan"
msgstr "Plan veröffentlichen"
#: AKModel/admin.py:113 AKModel/admin.py:129 AKModel/views/manage.py:151
#: AKModel/admin.py:115 AKModel/admin.py:131 AKModel/views/manage.py:151
msgid "Unpublish plan"
msgstr "Plan verbergen"
#: AKModel/admin.py:168 AKModel/models.py:381 AKModel/models.py:707
#: AKModel/admin.py:170 AKModel/models.py:381 AKModel/models.py:707
#: AKModel/templates/admin/AKModel/aks_by_user.html:12
#: AKModel/templates/admin/AKModel/status/event_aks.html:10
#: AKModel/views/manage.py:73 AKModel/views/status.py:102
msgid "AKs"
msgstr "AKs"
#: AKModel/admin.py:234
#: AKModel/admin.py:236
msgid "Wish"
msgstr "AK-Wunsch"
#: AKModel/admin.py:240
#: AKModel/admin.py:242
msgid "Is wish"
msgstr "Ist ein Wunsch"
#: AKModel/admin.py:241
#: AKModel/admin.py:243
msgid "Is not a wish"
msgstr "Ist kein Wunsch"
#: AKModel/admin.py:301
#: AKModel/admin.py:303
msgid "Export to wiki syntax"
msgstr "In Wiki-Syntax exportieren"
#: AKModel/admin.py:318
#: AKModel/admin.py:320
msgid "Cannot export AKs from more than one event at the same time."
msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
#: AKModel/admin.py:333 AKModel/views/ak.py:99
#: AKModel/admin.py:335 AKModel/views/ak.py:99
msgid "Reset interest in AKs"
msgstr "Interesse an AKs zurücksetzen"
#: AKModel/admin.py:343 AKModel/views/ak.py:114
#: AKModel/admin.py:345 AKModel/views/ak.py:114
msgid "Reset AKs' interest counters"
msgstr "Interessenszähler der AKs zurücksetzen"
#: AKModel/admin.py:442 AKModel/admin.py:456
#: AKModel/admin.py:444 AKModel/admin.py:458
msgid "AK Details"
msgstr "AK-Details"
#: AKModel/admin.py:518 AKModel/views/manage.py:99
#: AKModel/admin.py:520 AKModel/views/manage.py:99
msgid "Mark Constraint Violations as manually resolved"
msgstr "Markiere Constraintverletzungen als manuell behoben"
#: AKModel/admin.py:527 AKModel/views/manage.py:112
#: AKModel/admin.py:529 AKModel/views/manage.py:112
msgid "Set Constraint Violations to level \"violation\""
msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
#: AKModel/admin.py:536 AKModel/views/manage.py:125
#: AKModel/admin.py:538 AKModel/views/manage.py:125
msgid "Set Constraint Violations to level \"warning\""
msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
#: AKModel/admin.py:580
msgid "Activate selected users"
msgstr "Ausgewählte Benutzer*innen aktivieren"
#: AKModel/admin.py:583
msgid "The selected users have been activated."
msgstr "Benutzer*innen aktiviert"
#: AKModel/admin.py:585
msgid "Deactivate selected users"
msgstr "Ausgewählte Benutzer*innen deaktivieren"
#: AKModel/admin.py:588
msgid "The selected users have been deactivated."
msgstr "Benutzer*innen deaktiviert"
#: AKModel/availability/forms.py:25 AKModel/availability/models.py:271
msgid "Availability"
msgstr "Verfügbarkeit"
......
......@@ -408,8 +408,22 @@ class AK(models.Model):
detail_string += f"\n{_('Requirements')}: {', '.join(str(r) for r in self.requirements.all())}"
if self.types.count() > 0:
detail_string += f"\n{_('Types')}: {', '.join(str(r) for r in self.types.all())}"
# Find conflicts
# (both directions, those specified for this AK and those were this AK was specified as conflict)
# Deduplicate and order list alphabetically
conflicts = set()
if self.conflicts.count() > 0:
detail_string += f"\n{_('Conflicts')}: {', '.join(str(c) for c in self.conflicts.all())}"
for c in self.conflicts.all():
conflicts.add(str(c))
if self.conflict.count() > 0:
for c in self.conflict.all():
conflicts.add(str(c))
if len(conflicts) > 0:
conflicts = list(conflicts)
conflicts.sort()
detail_string += f"\n{_('Conflicts')}: {', '.join(conflicts)}"
if self.prerequisites.count() > 0:
detail_string += f"\n{_('Prerequisites')}: {', '.join(str(p) for p in self.prerequisites.all())}"
detail_string += f"\n{_('Availabilities')}: \n{availabilities}"
......
......@@ -24,7 +24,8 @@ class AKAddSlotForm(forms.Form):
start = forms.CharField(label=_("Start"), disabled=True)
end = forms.CharField(label=_("End"), disabled=True)
duration = forms.CharField(label=_("Duration"), disabled=True)
room = forms.IntegerField(label=_("Room"), disabled=True)
room = forms.IntegerField(label=_("Room"), disabled=True, widget=forms.HiddenInput())
room_name = forms.CharField(label=_("Room"), disabled=True)
def __init__(self, event):
super().__init__()
......
......@@ -157,6 +157,7 @@
$('#id_start').val(info.startStr);
$('#id_end').val(info.endStr);
$('#id_room').val(info.resource._resource.id);
$('#id_room_name').val(info.resource._resource.title);
$('#id_duration').val(Math.abs(info.end-info.start)/1000/60/60);
$('#id_ak').val("");
$('#newAKSlotModal').modal('show');
......@@ -354,9 +355,11 @@
<h5 class="mt-2">{{ track_slots.grouper }}</h5>
{% endif %}
{% for slot in track_slots.list %}
<div class="unscheduled-slot badge" style='background-color: {{ slot.ak.category.color }}'
data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "constraint": "roomAvailable", "description": "{{ slot.ak.details | escapejs }}", "slotID": "{{ slot.pk }}", "backgroundColor": "{{ slot.ak.category.color }}", "url": "{% url "admin:AKModel_akslot_change" slot.pk %}"}' data-details="{{ slot.ak.details }}">{{ slot.ak.short_name }}
<div class="unscheduled-slot badge" style='background-color: {% with slot.ak.category.color as color %} {% if color %}{{ color }}{% else %}#000000;{% endif %}{% endwith %}'
{% with slot.ak.details as details %}
data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "constraint": "roomAvailable", "description": "{{ details | escapejs }}", "slotID": "{{ slot.pk }}", "backgroundColor": "{{ slot.ak.category.color }}", "url": "{% url "admin:AKModel_akslot_change" slot.pk %}"}' data-details="{{ details }}">{{ slot.ak.short_name }}
({{ slot.duration }} h)<br>{{ slot.ak.owners_list }}
{% endwith %}
</div>
{% endfor %}
{% endfor %}
......
......@@ -13,14 +13,19 @@
{% block content %}
<h4 class="mt-4 mb-4">{% trans "AKs with public notes" %}</h4>
{% for ak in aks_with_comment %}
<a href="{{ ak.edit_url }}">{{ ak }}</a><br>{{ ak.notes }}<br><br>
<a href="{{ ak.detail_url }}">{{ ak }}</a>
<a href="{{ ak.edit_url }}">{% fa6_icon "pen-to-square" %}</a>
<a class="link-warning" href="{% url "admin:AKModel_ak_change" object_id=ak.pk %}">{% fa6_icon "pen-to-square" %}</a><br>
{{ ak.notes }}<br><br>
{% empty %}
-
{% endfor %}
<h4 class="mt-4 mb-4">{% trans "AKs without availabilities" %}</h4>
{% for ak in aks_without_availabilities %}
<a href="{{ ak.edit_url }}">{{ ak }}</a><br>
<a href="{{ ak.detail_url }}">{{ ak }}</a>
<a href="{{ ak.edit_url }}">{% fa6_icon "pen-to-square" %}</a>
<a class="link-warning" href="{% url "admin:AKModel_ak_change" object_id=ak.pk %}">{% fa6_icon "pen-to-square" %}</a><br>
{% empty %}
-<br>
{% endfor %}
......@@ -30,7 +35,10 @@
<h4 class="mt-4 mb-4">{% trans "AK wishes with slots" %}</h4>
{% for ak in ak_wishes_with_slots %}
<a href="{{ ak.detail_url }}">{{ ak }}</a> <a href="{% url "admin:AKModel_akslot_changelist" %}?ak={{ ak.pk }}">({{ ak.akslot__count }})</a><br>
<a href="{% url "admin:AKModel_akslot_changelist" %}?ak={{ ak.pk }}">({{ ak.akslot__count }})</a>
<a href="{{ ak.detail_url }}">{{ ak }}</a>
<a href="{{ ak.edit_url }}">{% fa6_icon "pen-to-square" %}</a>
<a class="link-warning" href="{% url "admin:AKModel_ak_change" object_id=ak.pk %}">{% fa6_icon "pen-to-square" %}</a><br>
{% empty %}
-<br>
{% endfor %}
......@@ -39,7 +47,9 @@
<h4 class="mt-4 mb-4">{% trans "AKs without slots" %}</h4>
{% for ak in aks_without_slots %}
<a href="{{ ak.detail_url }}">{{ ak }}</a><br>
<a href="{{ ak.detail_url }}">{{ ak }}</a>
<a href="{{ ak.edit_url }}">{% fa6_icon "pen-to-square" %}</a>
<a class="link-warning" href="{% url "admin:AKModel_ak_change" object_id=ak.pk %}">{% fa6_icon "pen-to-square" %}</a><br>
{% empty %}
-<br>
{% endfor %}
......
......@@ -41,7 +41,9 @@ class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
context_object_name = "slots_unscheduled"
def get_queryset(self):
return super().get_queryset().filter(start__isnull=True).select_related('event', 'ak').order_by('ak__track')
return super().get_queryset().filter(start__isnull=True).select_related('event', 'ak', 'ak__track',
'ak__category').prefetch_related('ak__types', 'ak__owners', 'ak__conflicts', 'ak__prerequisites',
'ak__requirements', 'ak__conflict').order_by('ak__track', 'ak')
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment