Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • komasolver
  • main
  • renovate/django_csp-4.x
3 results

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Select Git revision
Show changes
Showing
with 928 additions and 40 deletions
import csv
import datetime
import json
import os
import tempfile
from abc import ABC, abstractmethod
from itertools import zip_longest
import django.db
from django.contrib import admin, messages
from django.contrib import messages
from django.db.models.functions import Now
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy, reverse
from django.shortcuts import redirect
from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView
from django.views.generic import TemplateView, DetailView
from django_tex.core import render_template_with_context, run_tex_in_directory
from django_tex.response import PDFResponse
from rest_framework import viewsets, permissions, mixins
from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm, SlideExportForm, \
AdminIntermediateActionForm, DefaultSlotEditorForm, RoomBatchCreationForm
from AKModel.models import Event, AK, AKSlot, Room, AKTrack, AKCategory, AKOwner, AKOrgaMessage, AKRequirement, \
ConstraintViolation, DefaultSlot
from AKModel.serializers import AKSerializer, AKSlotSerializer, RoomSerializer, AKTrackSerializer, AKCategorySerializer, \
AKOwnerSerializer
from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONScheduleImportForm
from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
class EventSlugMixin:
"""
Mixin to handle views with event slugs
class UserView(TemplateView):
"""
event = None
def _load_event(self):
# Find event based on event slug
if self.event is None:
self.event = get_object_or_404(Event, slug=self.kwargs.get("event_slug", None))
def get(self, request, *args, **kwargs):
self._load_event()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self._load_event()
return super().post(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
self._load_event()
return super().list(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
self._load_event()
return super().create(request, *args, **kwargs)
def dispatch(self, request, *args, **kwargs):
if self.event is None:
self._load_event()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
# Add event to context (to make it accessible in templates)
context["event"] = self.event
return context
View: Start page for logged in user
class FilterByEventSlugMixin(EventSlugMixin):
Will over a link to backend or inform the user that their account still needs to be confirmed
"""
Mixin to filter different querysets based on a event slug from the request url
"""
def get_queryset(self):
# Filter current queryset based on url event slug or return 404 if event slug is invalid
return super().get_queryset().filter(event=self.event)
class AdminViewMixin:
site_url = ''
title = ''
def get_context_data(self, **kwargs):
extra = admin.site.each_context(self.request)
extra.update(super().get_context_data(**kwargs))
if self.site_url != '':
extra["site_url"] = self.site_url
if self.title != '':
extra["title"] = self.title
return extra
class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKOwnerSerializer
def get_queryset(self):
return AKOwner.objects.filter(event=self.event)
class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKCategorySerializer
def get_queryset(self):
return AKCategory.objects.filter(event=self.event)
class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKTrackSerializer
def get_queryset(self):
return AKTrack.objects.filter(event=self.event)
class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin,
viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKSerializer
def get_queryset(self):
return AK.objects.filter(event=self.event)
class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = RoomSerializer
def get_queryset(self):
return Room.objects.filter(event=self.event)
class AKSlotViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKSlotSerializer
def get_queryset(self):
return AKSlot.objects.filter(event=self.event)
class UserView(TemplateView):
template_name = "AKModel/user.html"
class EventStatusView(AdminViewMixin, DetailView):
template_name = "admin/AKModel/status.html"
model = Event
context_object_name = "event"
title = _("Event Status")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["unscheduled_slots_count"] = context["event"].akslot_set.filter(start=None).count
context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
context["ak_messages"] = AKOrgaMessage.objects.filter(ak__event=context["event"])
return context
class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
model = AKRequirement
context_object_name = "requirements"
title = _("Requirements for Event")
template_name = "admin/AKModel/requirements_overview.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["event"] = self.event
context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
return context
class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
template_name = "admin/AKModel/ak_csv_export.html"
model = AKSlot
context_object_name = "slots"
title = _("AK CSV Export")
def get_queryset(self):
return super().get_queryset().order_by("ak__track")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
class AKWikiExportView(AdminViewMixin, DetailView):
template_name = "admin/AKModel/wiki_export.html"
model = Event
context_object_name = "event"
title = _("AK Wiki Export")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
categories_with_aks, ak_wishes = context["event"].get_categories_with_aks(wishes_seperately=True)
context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks]
context["categories_with_aks"].append((_("Wishes"), ak_wishes))
return context
class IntermediateAdminView(AdminViewMixin, FormView):
template_name = "admin/AKModel/action_intermediate.html"
form_class = AdminIntermediateForm
def get_preview(self):
return ""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = self.title
context["preview"] = self.get_preview()
return context
class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
template_name = "admin/AKModel/message_delete.html"
title = _("Delete AK Orga Messages")
def get_orga_messages_for_event(self, event):
return AKOrgaMessage.objects.filter(ak__event=event)
def get_success_url(self):
return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["ak_messages"] = self.get_orga_messages_for_event(self.event)
return context
def form_valid(self, form):
self.get_orga_messages_for_event(self.event).delete()
messages.add_message(self.request, messages.SUCCESS, _("AK Orga Messages successfully deleted"))
return super().form_valid(form)
class WizardViewMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["wizard_step"] = self.wizard_step
context["wizard_steps"] = [
_("Start"),
_("Settings"),
_("Event created, Prepare Import"),
_("Import categories & requirements"),
_("Activate?"),
_("Finish")
]
context["wizard_step_text"] = context["wizard_steps"][self.wizard_step - 1]
context["wizard_steps_total"] = len(context["wizard_steps"])
return context
class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView):
model = Event
form_class = NewEventWizardStartForm
template_name = "admin/AKModel/event_wizard/start.html"
wizard_step = 1
class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView):
model = Event
form_class = NewEventWizardSettingsForm
template_name = "admin/AKModel/event_wizard/settings.html"
wizard_step = 2
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["timezone"] = context["form"].cleaned_data["timezone"]
return context
def get_success_url(self):
return reverse_lazy("admin:new_event_wizard_prepare_import", kwargs={"event_slug": self.object.slug})
class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView):
form_class = NewEventWizardPrepareImportForm
template_name = "admin/AKModel/event_wizard/created_prepare_import.html"
wizard_step = 3
def form_valid(self, form):
# Selected a valid event to import from? Use this to go to next step of wizard
return redirect("admin:new_event_wizard_import", event_slug=self.event.slug,
import_slug=form.cleaned_data["import_event"].slug)
class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView):
form_class = NewEventWizardImportForm
template_name = "admin/AKModel/event_wizard/import.html"
wizard_step = 4
def get_initial(self):
initial = super().get_initial()
initial["import_event"] = Event.objects.get(slug=self.kwargs["import_slug"])
return initial
def form_valid(self, form):
for import_type in ["import_categories", "import_requirements"]:
for import_obj in form.cleaned_data.get(import_type):
# clone existing entry
try:
import_obj.event = self.event
import_obj.pk = None
import_obj.save()
messages.add_message(self.request, messages.SUCCESS, _("Copied '%(obj)s'" % {'obj': import_obj}))
except BaseException as e:
messages.add_message(self.request, messages.ERROR,
_("Could not copy '%(obj)s' (%(error)s)" % {'obj': import_obj,
"error": str(e)}))
return redirect("admin:new_event_wizard_activate", slug=self.event.slug)
class NewEventWizardActivateView(WizardViewMixin, UpdateView):
model = Event
template_name = "admin/AKModel/event_wizard/activate.html"
form_class = NewEventWizardActivateForm
wizard_step = 5
def get_success_url(self):
return reverse_lazy("admin:new_event_wizard_finish", kwargs={"slug": self.object.slug})
class NewEventWizardFinishView(WizardViewMixin, DetailView):
model = Event
template_name = "admin/AKModel/event_wizard/finish.html"
wizard_step = 6
class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
"""
View: Export slides to present AKs
Over a form to choose some settings for the export and then generate the PDF
"""
title = _('Export AK Slides')
form_class = SlideExportForm
def form_valid(self, form):
# pylint: disable=invalid-name
template_name = 'admin/AKModel/export/slides.tex'
# Settings
NEXT_AK_LIST_LENGTH = form.cleaned_data['num_next']
RESULT_PRESENTATION_MODE = form.cleaned_data["presentation_mode"]
SPACE_FOR_NOTES_IN_WISHES = form.cleaned_data["wish_notes"]
......@@ -353,12 +56,18 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
}
def build_ak_list_with_next_aks(ak_list):
"""
Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting)
"""
next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=list())]
return list(zip_longest(ak_list, next_aks_list, fillvalue=[]))
categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter=lambda
# Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly
# be presented when restriction setting was chosen)
categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter_func=lambda
ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)))
# Create context for LaTeX rendering
context = {
'title': self.event.name,
'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
......@@ -378,63 +87,17 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
os.remove(f'{tempdir}/texput.tex')
pdf = run_tex_in_directory(source, tempdir, template_name=self.template_name)
# Show PDF file to the user (with a filename containing a timestamp to prevent confusions about the right
# version to use when generating multiple versions of the slides, e.g., because owners did last-minute changes
# to their AKs
timestamp = datetime.datetime.now(tz=self.event.timezone).strftime("%Y-%m-%d_%H_%M")
return PDFResponse(pdf, filename=f'{self.event.slug}_ak_slides_{timestamp}.pdf')
class IntermediateAdminActionView(IntermediateAdminView, ABC):
form_class = AdminIntermediateActionForm
entities = None
def get_queryset(self, pks=None):
if pks is None:
pks = self.request.GET['pks']
return self.model.objects.filter(pk__in=pks.split(","))
def get_initial(self):
initial = super().get_initial()
initial['pks'] = self.request.GET['pks']
return initial
def get_preview(self):
self.entities = self.get_queryset()
joined_entities = '\n'.join(str(e) for e in self.entities)
return f"{self.confirmation_message}:\n\n {joined_entities}"
def get_success_url(self):
return reverse(f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist")
@abstractmethod
def action(self, form):
pass
def form_valid(self, form):
self.entities = self.get_queryset(pks=form.cleaned_data['pks'])
self.action(form)
messages.add_message(self.request, messages.SUCCESS, self.success_message)
return super().form_valid(form)
class LoopActionMixin(ABC):
def action(self, form):
self.pre_action()
for entity in self.entities:
self.perform_action(entity)
entity.save()
self.post_action()
@abstractmethod
def perform_action(self, entity):
pass
def pre_action(self):
pass
def post_action(self):
pass
class CVMarkResolvedView(IntermediateAdminActionView):
"""
Admin action view: Mark one or multitple constraint violation(s) as resolved
"""
title = _('Mark Constraint Violations as manually resolved')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be marked as manually resolved")
......@@ -445,6 +108,9 @@ class CVMarkResolvedView(IntermediateAdminActionView):
class CVSetLevelViolationView(IntermediateAdminActionView):
"""
Admin action view: Set one or multitple constraint violation(s) as to level "violation"
"""
title = _('Set Constraint Violations to level "violation"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'violation'")
......@@ -455,6 +121,9 @@ class CVSetLevelViolationView(IntermediateAdminActionView):
class CVSetLevelWarningView(IntermediateAdminActionView):
"""
Admin action view: Set one or multitple constraint violation(s) as to level "warning"
"""
title = _('Set Constraint Violations to level "warning"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'warning'")
......@@ -464,27 +133,10 @@ class CVSetLevelWarningView(IntermediateAdminActionView):
self.entities.update(level=ConstraintViolation.ViolationLevel.WARNING)
class AKResetInterestView(IntermediateAdminActionView):
title = _("Reset interest in AKs")
model = AK
confirmation_message = _("Interest of the following AKs will be set to not filled (-1):")
success_message = _("Reset of interest in AKs successful.")
def action(self, form):
self.entities.update(interest=-1)
class AKResetInterestCounterView(IntermediateAdminActionView):
title = _("Reset AKs' interest counters")
model = AK
confirmation_message = _("Interest counter of the following AKs will be set to 0:")
success_message = _("AKs' interest counters set back to 0.")
def action(self, form):
self.entities.update(interest_counter=0)
class PlanPublishView(IntermediateAdminActionView):
"""
Admin action view: Publish the plan of one or multitple event(s)
"""
title = _('Publish plan')
model = Event
confirmation_message = _('Publish the plan(s) of:')
......@@ -495,6 +147,9 @@ class PlanPublishView(IntermediateAdminActionView):
class PlanUnpublishView(IntermediateAdminActionView):
"""
Admin action view: Unpublish the plan of one or multitple event(s)
"""
title = _('Unpublish plan')
model = Event
confirmation_message = _('Unpublish the plan(s) of:')
......@@ -505,6 +160,9 @@ class PlanUnpublishView(IntermediateAdminActionView):
class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
"""
Admin view: Allow to edit the default slots of an event
"""
template_name = "admin/AKModel/default_slot_editor.html"
form_class = DefaultSlotEditorForm
title = _("Edit Default Slots")
......@@ -532,13 +190,14 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
previous_slot_ids = set(s.id for s in self.event.defaultslot_set.all())
# Loop over inputs and update or add slots
for slot in default_slots_raw:
start = tz.localize(parse_datetime(slot["start"]))
end = tz.localize(parse_datetime(slot["end"]))
start = parse_datetime(slot["start"]).replace(tzinfo=tz)
end = parse_datetime(slot["end"]).replace(tzinfo=tz)
if slot["id"] != '':
id = int(slot["id"])
if id not in previous_slot_ids:
slot_id = int(slot["id"])
if slot_id not in previous_slot_ids:
# Make sure only slots (currently) belonging to this event are edited
# (user did not manipulate IDs and slots have not been deleted in another session in the meantime)
messages.add_message(
......@@ -549,8 +208,8 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
)
else:
# Update existing entries
previous_slot_ids.remove(id)
original_slot = DefaultSlot.objects.get(id=id)
previous_slot_ids.remove(slot_id)
original_slot = DefaultSlot.objects.get(id=slot_id)
if original_slot.start != start or original_slot.end != end:
original_slot.start = start
original_slot.end = end
......@@ -570,6 +229,7 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
for d_id in previous_slot_ids:
DefaultSlot.objects.get(id=d_id).delete()
# Inform user about changes performed
if created_count + updated_count + deleted_count > 0:
messages.add_message(
self.request,
......@@ -580,49 +240,36 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
return super().form_valid(form)
class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
form_class = RoomBatchCreationForm
title = _("Import Rooms from CSV")
def get_success_url(self):
return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug})
def form_valid(self, form):
from django.apps import apps
virtual_rooms_support = False
created_count = 0
class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
"""
View: Show all AKs of a given user
"""
model = AKOwner
context_object_name = 'owner'
template_name = "admin/AKModel/aks_by_user.html"
rooms_raw_dict: csv.DictReader = form.cleaned_data["rooms"]
if apps.is_installed("AKOnline") and "url" in rooms_raw_dict.fieldnames:
virtual_rooms_support = True
from AKOnline.models import VirtualRoom
class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView):
"""
View: Import an AK schedule from a json file that can be pasted into this view.
"""
template_name = "admin/AKModel/import_json.html"
form_class = JSONScheduleImportForm
title = _("AK Schedule JSON Import")
for raw_room in rooms_raw_dict:
name = raw_room["name"]
location = raw_room["location"] if "location" in rooms_raw_dict.fieldnames else ""
capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1
def form_valid(self, form):
try:
number_of_slots_changed = self.event.schedule_from_json(form.cleaned_data["data"])
messages.add_message(
self.request,
messages.SUCCESS,
_("Successfully imported {n} slot(s)").format(n=number_of_slots_changed)
)
except ValueError as ex:
messages.add_message(
self.request,
messages.ERROR,
_("Importing an AK schedule failed! Reason: ") + str(ex),
)
try:
if virtual_rooms_support and raw_room["url"] != "":
VirtualRoom.objects.create(name=name,
location=location,
capacity=capacity,
url=raw_room["url"],
event=self.event)
else:
Room.objects.create(name=name,
location=location,
capacity=capacity,
event=self.event)
created_count += 1
except django.db.Error as e:
messages.add_message(self.request, messages.WARNING,
_("Could not import room {name}: {e}").format(name=name, e=str(e)))
if created_count > 0:
messages.add_message(self.request, messages.SUCCESS,
_("Imported {count} room(s)").format(count=created_count))
else:
messages.add_message(self.request, messages.WARNING, _("No rooms imported"))
return super().form_valid(form)
return redirect("admin:event_status", self.event.slug)
import csv
import django.db
from django.apps import apps
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView
from AKModel.availability.models import Availability
from AKModel.forms import RoomForm, RoomBatchCreationForm
from AKModel.metaviews.admin import AdminViewMixin, EventSlugMixin, IntermediateAdminView
from AKModel.models import Room
class RoomCreationView(AdminViewMixin, CreateView):
"""
Admin view: Create a room
"""
form_class = RoomForm
template_name = 'admin/AKModel/room_create.html'
def get_success_url(self):
print(self.request.POST['save_action'])
if self.request.POST['save_action'] == 'save_add_another':
return reverse_lazy('admin:room-new')
if self.request.POST['save_action'] == 'save_continue':
return reverse_lazy('admin:AKModel_room_change', kwargs={'object_id': self.room.pk})
return reverse_lazy('admin:AKModel_room_changelist')
def form_valid(self, form):
self.room = form.save() # pylint: disable=attribute-defined-outside-init
# translatable string with placeholders, no f-string possible
# pylint: disable=consider-using-f-string
messages.success(self.request, _("Created Room '%(room)s'" % {'room': self.room}))
return HttpResponseRedirect(self.get_success_url())
class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
"""
Admin action: Allow to create rooms in batch by inputing a CSV-formatted list of room details into a textbox
This offers the input form, supports creation of virtual rooms if AKOnline is active, too,
and users can specify that default availabilities (from event start to end) should be created for the rooms
automatically
"""
form_class = RoomBatchCreationForm
title = _("Import Rooms from CSV")
def get_success_url(self):
return reverse_lazy('admin:event_status', kwargs={'event_slug': self.event.slug})
def form_valid(self, form):
virtual_rooms_support = False
create_default_availabilities = form.cleaned_data["create_default_availabilities"]
created_count = 0
rooms_raw_dict: csv.DictReader = form.cleaned_data["rooms"]
# Prepare creation of virtual rooms if there is information (an URL) in the data and the AKOnline app is active
if apps.is_installed("AKOnline") and "url" in rooms_raw_dict.fieldnames:
virtual_rooms_support = True
# pylint: disable=import-outside-toplevel
from AKOnline.models import VirtualRoom
# Loop over all inputs
for raw_room in rooms_raw_dict:
# Gather the relevant information (most fields can be empty)
name = raw_room["name"]
location = raw_room["location"] if "location" in rooms_raw_dict.fieldnames else ""
capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1
try:
# Try to create a room (catches cases where the room name contains keywords or symbols that the
# database cannot handle (.e.g., special UTF-8 characters)
r = Room.objects.create(name=name,
location=location,
capacity=capacity,
event=self.event)
# and if necessary an associated virtual room, too
if virtual_rooms_support and raw_room["url"] != "":
VirtualRoom.objects.create(room=r,
url=raw_room["url"])
# If user requested default availabilities, create them
if create_default_availabilities:
a = Availability.with_event_length(event=self.event, room=r)
a.save()
created_count += 1
except django.db.Error as e:
messages.add_message(self.request, messages.WARNING,
_("Could not import room {name}: {e}").format(name=name, e=str(e)))
# Inform the user about the rooms created
if created_count > 0:
messages.add_message(self.request, messages.SUCCESS,
_("Imported {count} room(s)").format(count=created_count))
else:
messages.add_message(self.request, messages.WARNING, _("No rooms imported"))
return super().form_valid(form)
from django.apps import apps
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from AKModel.metaviews import status_manager
from AKModel.metaviews.admin import EventSlugMixin
from AKModel.metaviews.status import TemplateStatusWidget, StatusView
@status_manager.register(name="event_overview")
class EventOverviewWidget(TemplateStatusWidget):
"""
Status page widget: Event overview
"""
required_context_type = "event"
title = _("Overview")
template_name = "admin/AKModel/status/event_overview.html"
def render_status(self, context: {}) -> str:
return "success" if not context["event"].plan_hidden else "primary"
@status_manager.register(name="event_categories")
class EventCategoriesWidget(TemplateStatusWidget):
"""
Status page widget: Category information
Show all categories of the event together with the number of AKs belonging to this category.
Offers an action to add a new category.
"""
required_context_type = "event"
title = _("Categories")
template_name = "admin/AKModel/status/event_categories.html"
actions = [
{
"text": _("Add category"),
"url": reverse_lazy("admin:AKModel_akcategory_add"),
}
]
def render_title(self, context: {}) -> str:
# Store category count as instance variable for re-use in body
self.category_count = context['event'].akcategory_set.count() # pylint: disable=attribute-defined-outside-init
return f"{super().render_title(context)} ({self.category_count})"
def render_status(self, context: {}) -> str:
return "danger" if self.category_count == 0 else "primary"
@status_manager.register(name="event_rooms")
class EventRoomsWidget(TemplateStatusWidget):
"""
Status page widget: Category information
Show all rooms of the event.
Offers actions to add a single new room as well as for batch creation.
"""
required_context_type = "event"
title = _("Rooms")
template_name = "admin/AKModel/status/event_rooms.html"
actions = [
{
"text": _("Add Room"),
"url": reverse_lazy("admin:AKModel_room_add"),
}
]
def render_title(self, context: {}) -> str:
# Store room count as instance variable for re-use in body
self.room_count = context['event'].room_set.count() # pylint: disable=attribute-defined-outside-init
return f"{super().render_title(context)} ({self.room_count})"
def render_status(self, context: {}) -> str:
return "danger" if self.room_count == 0 else "primary"
def render_actions(self, context: {}) -> list[dict]:
actions = super().render_actions(context)
# Action has to be added here since it depends on the event for URL building
import_room_url = reverse_lazy("admin:room-import", kwargs={"event_slug": context["event"].slug})
for action in actions:
if action["url"] == import_room_url:
return actions
actions.append(
{
"text": _("Import Rooms from CSV"),
"url": import_room_url,
}
)
return actions
@status_manager.register(name="event_aks")
class EventAKsWidget(TemplateStatusWidget):
"""
Status page widget: AK information
Show information about the AKs of this event.
Offers a long list of AK-related actions and also scheduling actions of AKScheduling is active
"""
required_context_type = "event"
title = _("AKs")
template_name = "admin/AKModel/status/event_aks.html"
def get_context_data(self, context) -> dict:
context["ak_count"] = context["event"].ak_set.count()
context["unscheduled_slots_count"] = context["event"].akslot_set.filter(start=None).count
return context
def render_actions(self, context: {}) -> list[dict]:
actions = [
{
"text": _("Scheduling"),
"url": reverse_lazy("admin:schedule", kwargs={"event_slug": context["event"].slug}),
},
]
if apps.is_installed("AKScheduling"):
actions.extend([
{
"text": _("AKs requiring special attention"),
"url": reverse_lazy("admin:special-attention", kwargs={"slug": context["event"].slug}),
},
])
if context["event"].ak_set.count() > 0:
actions.append({
"text": _("Enter Interest"),
"url": reverse_lazy("admin:enter-interest",
kwargs={"event_slug": context["event"].slug,
"pk": context["event"].ak_set.all().first().pk}
),
})
actions.extend([
{
"text": _("Edit Default Slots"),
"url": reverse_lazy("admin:default-slots-editor", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Manage ak tracks"),
"url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Import AK schedule from JSON"),
"url": reverse_lazy("admin:ak_schedule_json_import", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Export AKs as CSV"),
"url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Export AKs as JSON"),
"url": reverse_lazy("admin:ak_json_export", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Export AKs for Wiki"),
"url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}),
},
{
"text": _("Export AK Slides"),
"url": reverse_lazy("admin:ak_slide_export", kwargs={"event_slug": context["event"].slug}),
},
]
)
return actions
@status_manager.register(name="event_requirements")
class EventRequirementsWidget(TemplateStatusWidget):
"""
Status page widget: Requirement information information
Show information about the requirements of this event.
Offers actions to add new requirements or to get a list of AKs having a given requirement.
"""
required_context_type = "event"
title = _("Requirements")
template_name = "admin/AKModel/status/event_requirements.html"
def render_title(self, context: {}) -> str:
# Store requirements count as instance variable for re-use in body
# pylint: disable=attribute-defined-outside-init
self.requirements_count = context['event'].akrequirement_set.count()
return f"{super().render_title(context)} ({self.requirements_count})"
def render_actions(self, context: {}) -> list[dict]:
return [
{
"text": _("Show AKs for requirements"),
"url": reverse_lazy("admin:event_requirement_overview", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Add Requirement"),
"url": reverse_lazy("admin:AKModel_akrequirement_add"),
},
]
class EventStatusView(EventSlugMixin, StatusView):
"""
View: Show a status dashboard for the given event
"""
title = _("Event Status")
provided_context_type = "event"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
return context
from django.contrib import admin
from AKModel.admin import RoomAdmin, RoomForm
from AKOnline.models import VirtualRoom
class VirtualRoomForm(RoomForm):
class Meta(RoomForm.Meta):
model = VirtualRoom
fields = ['name',
'location',
'url',
'capacity',
'properties',
'event',
]
@admin.register(VirtualRoom)
class VirtualRoomAdmin(RoomAdmin):
class VirtualRoomAdmin(admin.ModelAdmin):
"""
Admin interface for virtual room model
"""
model = VirtualRoom
list_display = ['room', 'event', 'url']
list_filter = ['room__event']
def get_form(self, request, obj=None, change=False, **kwargs):
if obj is not None:
return VirtualRoomForm
return super().get_form(request, obj, change, **kwargs)
def get_readonly_fields(self, request, obj=None):
# Don't allow changing the room on existing virtual rooms
# Instead, a link to the room editing form will be displayed automatically
if obj:
return self.readonly_fields + ('room', )
return self.readonly_fields
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkonlineConfig(AppConfig):
"""
App configuration (default -- only to set the app name)
"""
name = 'AKOnline'
from betterforms.multiform import MultiModelForm
from django.forms import ModelForm
from AKModel.forms import RoomForm
from AKOnline.models import VirtualRoom
class VirtualRoomForm(ModelForm):
"""
Form to create a virtual room
Should be used as part of a multi form (see :class:`RoomWithVirtualForm` below)
"""
class Meta:
model = VirtualRoom
# Show all fields except for room
exclude = ['room'] #pylint: disable=modelform-uses-exclude
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the URL field optional to allow submitting the multi form without creating a virtual room
self.fields['url'].required = False
class RoomWithVirtualForm(MultiModelForm):
"""
Combined form to create rooms and optionally virtual rooms
Multi-Form that combines a :class:`RoomForm` (from AKModel) and a :class:`VirtualRoomForm` (see above).
The form will always create a room on valid input
and may additionally create a virtual room if the url field of the virtual room form part is set.
"""
form_classes = {
'room': RoomForm,
'virtual': VirtualRoomForm
}
def save(self, commit=True):
objects = super().save(commit=False)
if commit:
room = objects['room']
room.save()
virtual = objects['virtual']
if virtual.url != "":
virtual.room = room
virtual.save()
else:
objects['virtual'] = None
return objects
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-16 16:30+0200\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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: AKOnline/models.py:11
msgid "URL"
msgstr "URL"
#: AKOnline/models.py:11
msgid "URL to the room or server"
msgstr "URL zum Raum oder Server"
#: AKOnline/models.py:12
msgid "Room"
msgstr "Raum"
#: AKOnline/models.py:16
#: AKOnline/templates/admin/AKOnline/room_create_with_virtual.html:10
msgid "Virtual Room"
msgstr "Virtueller Raum"
#: AKOnline/models.py:17 AKOnline/views.py:38
msgid "Virtual Rooms"
msgstr "Virtuelle Räume"
#: AKOnline/templates/admin/AKOnline/room_create_with_virtual.html:11
msgid "Leave empty if that room is not virtual/hybrid."
msgstr "Leer lassen wenn der Raum nicht virtuell/hybrid ist"
#: AKOnline/views.py:25
#, python-format
msgid "Created Room '%(room)s'"
msgstr "Raum '%(room)s' angelegt"
#: AKOnline/views.py:28
#, python-format
msgid "Created related Virtual Room '%(vroom)s'"
msgstr "Verbundenen virtuellen Raum '%(vroom)s' angelegt"
# Generated by Django 4.1.5 on 2023-03-22 12:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
replaces = [('AKOnline', '0001_AKOnline'), ('AKOnline', '0002_rework_virtual'), ('AKOnline', '0003_rework_virtual_2'), ('AKOnline', '0004_rework_virtual_3'), ('AKOnline', '0005_rework_virtual_4')]
initial = True
dependencies = [
('AKModel', '0033_AKOnline'),
('AKModel', '0057_upgrades'),
]
operations = [
migrations.CreateModel(
name='VirtualRoom',
fields=[
('room', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='virtual', serialize=False, to='AKModel.room', verbose_name='Room')),
('url', models.URLField(blank=True, help_text='URL to the room or server', verbose_name='URL')),
],
options={
'verbose_name': 'Virtual Room',
'verbose_name_plural': 'Virtual Rooms',
},
),
]
# Generated by Django 4.1.5 on 2023-03-21 23:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0057_upgrades'),
('AKOnline', '0001_AKOnline'),
]
operations = [
migrations.RenameModel(
'VirtualRoom',
'VirtualRoomOld'
),
]
# Generated by Django 4.1.5 on 2023-03-21 23:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('AKOnline', '0002_rework_virtual'),
]
operations = [
migrations.CreateModel(
name='VirtualRoom',
fields=[
('room', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True,
related_name='virtual', serialize=False, to='AKModel.room',
verbose_name='Room')),
('url', models.URLField(blank=True, help_text='URL to the room or server', verbose_name='URL')),
],
options={
'verbose_name': 'Virtual Room',
'verbose_name_plural': 'Virtual Rooms',
},
),
]
# Generated by Django 4.1.5 on 2023-03-21 23:21
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
dependencies = [
('AKOnline', '0003_rework_virtual_2'),
]
def copy_rooms(apps, schema_editor):
VirtualRoomOld = apps.get_model('AKOnline', 'VirtualRoomOld')
VirtualRoom = apps.get_model('AKOnline', 'VirtualRoom')
for row in VirtualRoomOld.objects.all():
v = VirtualRoom(room_id=row.pk, url=row.url)
v.save()
operations = [
migrations.RunPython(
copy_rooms,
reverse_code=migrations.RunPython.noop,
elidable=True,
)
]
# Generated by Django 4.1.5 on 2023-03-21 23:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('AKOnline', '0004_rework_virtual_3'),
]
operations = [
migrations.DeleteModel(
name='VirtualRoomOld',
),
]
from django.db import models
from django.utils.translation import gettext_lazy as _
from AKModel.models import Event, Room
from AKModel.models import Room
class VirtualRoom(Room):
""" A virtual room where an AK can be held.
class VirtualRoom(models.Model):
"""
Add details about a virtual or hybrid version of a room to it
"""
url = models.URLField(verbose_name=_("URL"), help_text=_("URL to the room or server"), blank=True)
room = models.OneToOneField(Room, verbose_name=_("Room"), on_delete=models.CASCADE,
related_name='virtual', primary_key=True)
class Meta:
verbose_name = _('Virtual Room')
verbose_name_plural = _('Virtual Rooms')
@property
def event(self):
"""
Property: Event this virtual room belongs to.
:return: Event this virtual room belongs to
:rtype: Event
"""
return self.room.event
def __str__(self):
return f"{self.room.title}: {self.url}"
{% extends "admin/AKModel/room_create.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load django_bootstrap5 %}
{% block form_details %}
{% bootstrap_form form.room %}
<h3>{% trans "Virtual Room" %}</h3>
<p>{% trans "Leave empty if that room is not virtual/hybrid." %}</p>
{% bootstrap_form form.virtual %}
{% endblock %}
{% load i18n %}
<ul>
{% for room in event.room_set.all %}
{% if room.virtual and room.virtual.url %}
<li>
<a href="{% url 'admin:AKOnline_virtualroom_change' room.pk %}">{{ room }} ({{ room.virtual.url | truncatechars:30 }})</a>
</li>
{% endif %}
{% endfor %}
</ul>
# Create your tests here.
# Create your views here.
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from AKModel.metaviews import status_manager
from AKModel.metaviews.status import TemplateStatusWidget
from AKModel.views.room import RoomCreationView
from AKOnline.forms import RoomWithVirtualForm
class RoomCreationWithVirtualView(RoomCreationView):
"""
View to create both rooms and optionally virtual rooms by filling one form
"""
form_class = RoomWithVirtualForm
template_name = 'admin/AKOnline/room_create_with_virtual.html'
room = None
def form_valid(self, form):
# This will create the room and additionally a virtual room if the url field is not blank
# objects['room'] will always a room instance afterwards, objects['virtual'] may be empty
objects = form.save()
self.room = objects['room']
# Create a (translated) success message containing information about the created room
messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']})) #pylint: disable=consider-using-f-string, line-too-long
if objects['virtual'] is not None:
# Create a (translated) success message containing information about the created virtual room
messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']})) #pylint: disable=consider-using-f-string, line-too-long
return HttpResponseRedirect(self.get_success_url())
@status_manager.register(name="event_virtual_rooms")
class EventVirtualRoomsWidget(TemplateStatusWidget):
"""
Status page widget to contain information about all virtual rooms belonging to the given event
"""
required_context_type = "event"
title = _("Virtual Rooms")
template_name = "admin/AKOnline/status/event_virtual_rooms.html"
# Register your models here.
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkplanConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = 'AKPlan'
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-05-22 08:02+0000\n"
"POT-Creation-Date: 2023-05-15 20:03+0200\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"
......@@ -27,56 +27,56 @@ msgstr "Plan"
msgid "Write to organizers of this event for questions and comments"
msgstr "Fragen oder Kommentare? Schreib den Orgas dieses Events eine Mail"
#: AKPlan/templates/AKPlan/plan_index.html:32
#: AKPlan/templates/AKPlan/plan_index.html:36
msgid "Day"
msgstr "Tag"
#: AKPlan/templates/AKPlan/plan_index.html:42
#: AKPlan/templates/AKPlan/plan_index.html:46
msgid "Event"
msgstr "Veranstaltung"
#: AKPlan/templates/AKPlan/plan_index.html:55
#: AKPlan/templates/AKPlan/plan_index.html:59
#: AKPlan/templates/AKPlan/plan_room.html:13
#: AKPlan/templates/AKPlan/plan_room.html:59
#: AKPlan/templates/AKPlan/plan_wall.html:52
#: AKPlan/templates/AKPlan/plan_wall.html:65
msgid "Room"
msgstr "Raum"
#: AKPlan/templates/AKPlan/plan_index.html:76
#: AKPlan/templates/AKPlan/plan_index.html:80
#: AKPlan/templates/AKPlan/plan_room.html:11
#: AKPlan/templates/AKPlan/plan_track.html:9
msgid "AK Plan"
msgstr "AK-Plan"
#: AKPlan/templates/AKPlan/plan_index.html:88
#: AKPlan/templates/AKPlan/plan_index.html:92
#: AKPlan/templates/AKPlan/plan_room.html:49
msgid "Rooms"
msgstr "Räume"
#: AKPlan/templates/AKPlan/plan_index.html:101
#: AKPlan/templates/AKPlan/plan_index.html:105
#: AKPlan/templates/AKPlan/plan_track.html:36
msgid "Tracks"
msgstr "Tracks"
#: AKPlan/templates/AKPlan/plan_index.html:113
#: AKPlan/templates/AKPlan/plan_index.html:117
msgid "AK Wall"
msgstr "AK-Wall"
#: AKPlan/templates/AKPlan/plan_index.html:126
#: AKPlan/templates/AKPlan/plan_wall.html:116
#: AKPlan/templates/AKPlan/plan_index.html:130
#: AKPlan/templates/AKPlan/plan_wall.html:130
msgid "Current AKs"
msgstr "Aktuelle AKs"
#: AKPlan/templates/AKPlan/plan_index.html:133
#: AKPlan/templates/AKPlan/plan_wall.html:121
#: AKPlan/templates/AKPlan/plan_index.html:137
#: AKPlan/templates/AKPlan/plan_wall.html:135
msgid "Next AKs"
msgstr "Nächste AKs"
#: AKPlan/templates/AKPlan/plan_index.html:141
#: AKPlan/templates/AKPlan/plan_index.html:145
msgid "This event is not active."
msgstr "Dieses Event ist nicht aktiv."
#: AKPlan/templates/AKPlan/plan_index.html:154
#: AKPlan/templates/AKPlan/plan_index.html:158
#: AKPlan/templates/AKPlan/plan_room.html:77
#: AKPlan/templates/AKPlan/plan_track.html:58
msgid "Plan is not visible (yet)."
......@@ -99,7 +99,7 @@ msgstr "Eigenschaften"
msgid "Track"
msgstr "Track"
#: AKPlan/templates/AKPlan/plan_wall.html:131
#: AKPlan/templates/AKPlan/plan_wall.html:145
msgid "Reload page automatically?"
msgstr "Seite automatisch neu laden?"
......