From bb57c17e11a8762a35d02d397feecb331b06b45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Thu, 23 Mar 2023 00:48:33 +0100 Subject: [PATCH] Improve virtual rooms Use One2One relationship instead of inheritance This allows to add and also remove the virtual features of a room Introduce new model Create migrations to migrate from existing to new model + a squashed version knowing only about the new model for fresh installations Adapt admin views Add django-betterforms as dependency to simply create a form allowing to generate rooms with optionally a virtual component in a single view and create that form Add view to create rooms in that view and use instead of default django creation form Use the new name/structure in templates Move RoomForm to forms.py to make imports easier Update batch creation of rooms This resolves #150 and also resolves #179 since now rooms and virtual rooms (rooms with virtual features) are created using the same view --- AKModel/admin.py | 42 +++++++------------ AKModel/forms.py | 35 +++++++++++++++- .../templates/admin/AKModel/room_create.html | 25 +++++++++++ AKModel/views.py | 39 ++++++++++++----- AKOnline/admin.py | 27 ++++-------- AKOnline/forms.py | 36 ++++++++++++++++ AKOnline/migrations/0001_virtualroom_new.py | 30 +++++++++++++ AKOnline/migrations/0002_rework_virtual.py | 19 +++++++++ AKOnline/migrations/0003_rework_virtual_2.py | 27 ++++++++++++ AKOnline/migrations/0004_rework_virtual_3.py | 28 +++++++++++++ AKOnline/migrations/0005_rework_virtual_4.py | 16 +++++++ AKOnline/models.py | 11 ++++- .../AKOnline/room_create_with_virtual.html | 26 ++++++++++++ AKPlan/templates/AKPlan/plan_room.html | 4 +- .../templates/AKSubmission/ak_detail.html | 8 ++-- requirements.txt | 1 + 16 files changed, 309 insertions(+), 65 deletions(-) create mode 100644 AKModel/templates/admin/AKModel/room_create.html create mode 100644 AKOnline/forms.py create mode 100644 AKOnline/migrations/0001_virtualroom_new.py create mode 100644 AKOnline/migrations/0002_rework_virtual.py create mode 100644 AKOnline/migrations/0003_rework_virtual_2.py create mode 100644 AKOnline/migrations/0004_rework_virtual_3.py create mode 100644 AKOnline/migrations/0005_rework_virtual_4.py create mode 100644 AKOnline/templates/admin/AKOnline/room_create_with_virtual.html diff --git a/AKModel/admin.py b/AKModel/admin.py index a6aae5ae..08abba39 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -13,13 +13,14 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse from simple_history.admin import SimpleHistoryAdmin -from AKModel.availability.forms import AvailabilitiesFormMixin from AKModel.availability.models import Availability +from AKModel.forms import RoomFormWithAvailabilities from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \ ConstraintViolation, DefaultSlot from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event from AKModel.views import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView, AKResetInterestView, \ - AKResetInterestCounterView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, RoomBatchCreationView + AKResetInterestCounterView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, RoomBatchCreationView, \ + RoomCreationView class EventRelatedFieldListFilter(RelatedFieldListFilter): @@ -235,30 +236,6 @@ class AKAdmin(SimpleHistoryAdmin): return HttpResponseRedirect(f"{reverse_lazy('admin:ak-reset-interest-counter')}?pks={','.join(str(pk) for pk in selected)}") -class RoomForm(AvailabilitiesFormMixin, forms.ModelForm): - class Meta: - model = Room - fields = ['name', - 'location', - 'capacity', - 'properties', - 'event', - ] - - widgets = { - 'properties': forms.CheckboxSelectMultiple, - } - - def __init__(self, *args, **kwargs): - # Init availability mixin - kwargs['initial'] = dict() - super().__init__(*args, **kwargs) - self.initial = {**self.initial, **kwargs['initial']} - # Filter possible values for m2m when event is specified - if hasattr(self.instance, "event") and self.instance.event is not None: - self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) - - @admin.register(Room) class RoomAdmin(admin.ModelAdmin): model = Room @@ -268,9 +245,13 @@ class RoomAdmin(admin.ModelAdmin): ordering = ['location', 'name'] change_form_template = "admin/AKModel/room_change_form.html" + def add_view(self, request, form_url='', extra_context=None): + # Use custom view for room creation (either room form or combined form if virtual rooms are supported) + return redirect("admin:room-new") + def get_form(self, request, obj=None, change=False, **kwargs): if obj is not None: - return RoomForm + return RoomFormWithAvailabilities return super().get_form(request, obj, change, **kwargs) def formfield_for_foreignkey(self, db_field, request, **kwargs): @@ -280,6 +261,13 @@ class RoomAdmin(admin.ModelAdmin): db_field, request, **kwargs ) + def get_urls(self): + urls = [ + path('new/', self.admin_site.admin_view(RoomCreationView.as_view()), name="room-new"), + ] + urls.extend(super().get_urls()) + return urls + class AKSlotAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): diff --git a/AKModel/forms.py b/AKModel/forms.py index bfa327e8..83d084da 100644 --- a/AKModel/forms.py +++ b/AKModel/forms.py @@ -6,7 +6,8 @@ from django import forms from django.forms.utils import ErrorList from django.utils.translation import gettext_lazy as _ -from AKModel.models import Event, AKCategory, AKRequirement +from AKModel.availability.forms import AvailabilitiesFormMixin +from AKModel.models import Event, AKCategory, AKRequirement, Room class NewEventWizardStartForm(forms.ModelForm): @@ -148,3 +149,35 @@ class RoomBatchCreationForm(AdminIntermediateForm): raise forms.ValidationError(_("CSV must contain a name column")) return rooms_raw_dict + + +class RoomForm(forms.ModelForm): + class Meta: + model = Room + fields = ['name', + 'location', + 'capacity', + 'properties', + 'event', + ] + + widgets = { + 'properties': forms.CheckboxSelectMultiple, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Filter possible values for m2m when event is specified + if hasattr(self.instance, "event") and self.instance.event is not None: + self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) + + def save(self, commit=True): + return super().save(commit) + + +class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): + def __init__(self, *args, **kwargs): + # Init availability mixin + kwargs['initial'] = dict() + super().__init__(*args, **kwargs) + self.initial = {**self.initial, **kwargs['initial']} diff --git a/AKModel/templates/admin/AKModel/room_create.html b/AKModel/templates/admin/AKModel/room_create.html new file mode 100644 index 00000000..58ebdae6 --- /dev/null +++ b/AKModel/templates/admin/AKModel/room_create.html @@ -0,0 +1,25 @@ +{% extends "admin/base_site.html" %} +{% load tags_AKModel %} + +{% load i18n %} +{% load django_bootstrap5 %} +{% load fontawesome_6 %} + + +{% block title %}{{event}}: {{ title }}{% endblock %} + +{% block content %} + <h2>{% trans "Create room" %}</h2> + <form method="post">{% csrf_token %} + {% bootstrap_form form %} + + <div class="float-end"> + <button type="submit" class="save btn btn-success" value="Submit"> + {% fa6_icon "check" 'fas' %} {% trans "Add" %} + </button> + </div> + <a href="javascript:history.back()" class="btn btn-info"> + {% fa6_icon "times" 'fas' %} {% trans "Cancel" %} + </a> + </form> +{% endblock %} diff --git a/AKModel/views.py b/AKModel/views.py index 68da9f43..9587704f 100644 --- a/AKModel/views.py +++ b/AKModel/views.py @@ -21,7 +21,7 @@ from rest_framework import viewsets, permissions, mixins from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \ NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm, SlideExportForm, \ - AdminIntermediateActionForm, DefaultSlotEditorForm, RoomBatchCreationForm + AdminIntermediateActionForm, DefaultSlotEditorForm, RoomBatchCreationForm, RoomForm from AKModel.models import Event, AK, AKSlot, Room, AKTrack, AKCategory, AKOwner, AKOrgaMessage, AKRequirement, \ ConstraintViolation, DefaultSlot from AKModel.serializers import AKSerializer, AKSlotSerializer, RoomSerializer, AKTrackSerializer, AKCategorySerializer, \ @@ -588,6 +588,26 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView): return super().form_valid(form) +class RoomCreationView(AdminViewMixin, CreateView): + success_url = reverse_lazy('admin:AKModel_room_changelist') # TODO Go to change view of created Room? + + def get_form_class(self): + if apps.is_installed("AKOnline"): + from AKOnline.forms import RoomWithVirtualForm + return RoomWithVirtualForm + return RoomForm + + def get_template_names(self): + if apps.is_installed("AKOnline"): + return 'admin/AKOnline/room_create_with_virtual.html' + return 'admin/AKModel/room_create.html' + + def form_valid(self, form): + r = super().form_valid(form) + messages.success(self.request, _("Created room")) # TODO Improve message, add detail information about room and if applicable virtual room + return r + + class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView): form_class = RoomBatchCreationForm title = _("Import Rooms from CSV") @@ -611,17 +631,14 @@ class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView): capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1 try: + # TODO Test + r = Room.objects.create(name=name, + location=location, + capacity=capacity, + event=self.event) 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) + VirtualRoom.objects.create(room=r, + url=raw_room["url"]) created_count += 1 except django.db.Error as e: messages.add_message(self.request, messages.WARNING, diff --git a/AKOnline/admin.py b/AKOnline/admin.py index a6d94333..69f4bed7 100644 --- a/AKOnline/admin.py +++ b/AKOnline/admin.py @@ -1,26 +1,17 @@ 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): 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 diff --git a/AKOnline/forms.py b/AKOnline/forms.py new file mode 100644 index 00000000..4a84958c --- /dev/null +++ b/AKOnline/forms.py @@ -0,0 +1,36 @@ +from betterforms.multiform import MultiModelForm +from django.forms import ModelForm + +from AKModel.forms import RoomForm +from AKOnline.models import VirtualRoom + + +class VirtualRoomForm(ModelForm): + class Meta: + model = VirtualRoom + exclude = ['room'] + + def __init__(self, *args, **kwargs): + super(VirtualRoomForm, self).__init__(*args, **kwargs) + self.fields['url'].required = False + + +class RoomWithVirtualForm(MultiModelForm): + form_classes = { + 'room': RoomForm, + 'virtual': VirtualRoomForm + } + + def save(self, commit=True): + objects = super(RoomWithVirtualForm, self).save(commit=False) + + if commit: + room = objects['room'] + room.save() + + virtual = objects['virtual'] + if virtual.url != "": + virtual.room = room + virtual.save() + + return objects diff --git a/AKOnline/migrations/0001_virtualroom_new.py b/AKOnline/migrations/0001_virtualroom_new.py new file mode 100644 index 00000000..941441d4 --- /dev/null +++ b/AKOnline/migrations/0001_virtualroom_new.py @@ -0,0 +1,30 @@ +# 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', + }, + ), + ] diff --git a/AKOnline/migrations/0002_rework_virtual.py b/AKOnline/migrations/0002_rework_virtual.py new file mode 100644 index 00000000..0b4d69be --- /dev/null +++ b/AKOnline/migrations/0002_rework_virtual.py @@ -0,0 +1,19 @@ +# 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' + ), + + ] diff --git a/AKOnline/migrations/0003_rework_virtual_2.py b/AKOnline/migrations/0003_rework_virtual_2.py new file mode 100644 index 00000000..5a996bdf --- /dev/null +++ b/AKOnline/migrations/0003_rework_virtual_2.py @@ -0,0 +1,27 @@ +# 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', + }, + ), + ] diff --git a/AKOnline/migrations/0004_rework_virtual_3.py b/AKOnline/migrations/0004_rework_virtual_3.py new file mode 100644 index 00000000..b92d61a4 --- /dev/null +++ b/AKOnline/migrations/0004_rework_virtual_3.py @@ -0,0 +1,28 @@ +# 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, + ) + ] diff --git a/AKOnline/migrations/0005_rework_virtual_4.py b/AKOnline/migrations/0005_rework_virtual_4.py new file mode 100644 index 00000000..1c5d2eae --- /dev/null +++ b/AKOnline/migrations/0005_rework_virtual_4.py @@ -0,0 +1,16 @@ +# 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', + ), + ] diff --git a/AKOnline/models.py b/AKOnline/models.py index c3496b01..bd3bd8cf 100644 --- a/AKOnline/models.py +++ b/AKOnline/models.py @@ -4,11 +4,18 @@ from django.utils.translation import gettext_lazy as _ from AKModel.models import Event, 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): + return self.room.event diff --git a/AKOnline/templates/admin/AKOnline/room_create_with_virtual.html b/AKOnline/templates/admin/AKOnline/room_create_with_virtual.html new file mode 100644 index 00000000..e352a121 --- /dev/null +++ b/AKOnline/templates/admin/AKOnline/room_create_with_virtual.html @@ -0,0 +1,26 @@ +{% extends "admin/base_site.html" %} +{% load tags_AKModel %} + +{% load i18n %} +{% load django_bootstrap5 %} +{% load fontawesome_6 %} + + +{% block title %}{{event}}: {{ title }}{% endblock %} + +{% block content %} + <h2>{% trans "Create room" %}</h2> + <form method="post">{% csrf_token %} + {% bootstrap_form form.room %} + {% bootstrap_form form.virtual %} + + <div class="float-end"> + <button type="submit" class="save btn btn-success" value="Submit"> + {% fa6_icon "check" 'fas' %} {% trans "Add" %} + </button> + </div> + <a href="javascript:history.back()" class="btn btn-info"> + {% fa6_icon "times" 'fas' %} {% trans "Cancel" %} + </a> + </form> +{% endblock %} diff --git a/AKPlan/templates/AKPlan/plan_room.html b/AKPlan/templates/AKPlan/plan_room.html index 6620a5d7..430fd9b0 100644 --- a/AKPlan/templates/AKPlan/plan_room.html +++ b/AKPlan/templates/AKPlan/plan_room.html @@ -58,8 +58,8 @@ <h1>{% trans "Room" %}: {{ room.name }} {% if room.location != '' %}({{ room.location }}){% endif %}</h1> - {% if "AKOnline"|check_app_installed and room.virtualroom and room.virtualroom.url != '' %} - <a class="btn btn-success" target="_parent" href="{{ room.virtualroom.url }}"> + {% if "AKOnline"|check_app_installed and room.virtual and room.virtual.url != '' %} + <a class="btn btn-success" target="_parent" href="{{ room.virtual.url }}"> {% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %} </a> {% endif %} diff --git a/AKSubmission/templates/AKSubmission/ak_detail.html b/AKSubmission/templates/AKSubmission/ak_detail.html index 4cb54ebe..eba22297 100644 --- a/AKSubmission/templates/AKSubmission/ak_detail.html +++ b/AKSubmission/templates/AKSubmission/ak_detail.html @@ -144,8 +144,8 @@ {% endblocktrans %} {% endif %} - {% if "AKOnline"|check_app_installed and featured_slot.room.virtualroom and featured_slot.room.virtualroom.url != '' %} - <a class="btn btn-success" target="_parent" href="{{ featured_slot.room.virtualroom.url }}"> + {% if "AKOnline"|check_app_installed and featured_slot.room.virtual and featured_slot.room.virtual.url != '' %} + <a class="btn btn-success" target="_parent" href="{{ featured_slot.room.virtual.url }}"> {% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %} </a> {% endif %} @@ -272,8 +272,8 @@ data-bs-toggle="tooltip" title="{% trans 'Delete' %}" class="btn btn-danger">{% fa6_icon 'times' 'fas' %}</a> {% else %} - {% if "AKOnline"|check_app_installed and slot.room and slot.room.virtualroom and slot.room.virtualroom.url != '' %} - <a class="btn btn-success" target="_parent" href="{{ slot.room.virtualroom.url }}"> + {% if "AKOnline"|check_app_installed and slot.room and slot.room.virtual and slot.room.virtual.url != '' %} + <a class="btn btn-success" target="_parent" href="{{ slot.room.virtual.url }}"> {% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %} </a> {% endif %} diff --git a/requirements.txt b/requirements.txt index 43194a7d..64081f5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,6 @@ django-tex==1.1.10 django-csp==3.7 django-compressor==4.1 django-libsass==0.9 +django-betterforms==2.0.0 mysqlclient==2.1.1 # for production deployment tzdata==2022.7 -- GitLab