Skip to content
Snippets Groups Projects
Commit f894e1a8 authored by Benjamin Hättasch's avatar Benjamin Hättasch
Browse files

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
parent 9cc32b5e
No related branches found
No related tags found
1 merge request!160Improve virtual rooms
Pipeline #151816 failed
Showing
with 1841 additions and 277 deletions
......@@ -13,8 +13,8 @@ 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
......@@ -235,30 +235,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 +244,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 +260,18 @@ class RoomAdmin(admin.ModelAdmin):
db_field, request, **kwargs
)
def get_urls(self):
if apps.is_installed("AKOnline"):
from AKOnline.views import RoomCreationWithVirtualView as RoomCreationView
else:
from .views import RoomCreationView
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):
......
......@@ -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,37 @@ 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',
'event',
]
class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
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)
This diff is collapsed.
{% extends "admin/base_site.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load django_bootstrap5 %}
{% load fontawesome_6 %}
{% block title %}{% trans "Create room" %}{% endblock %}
{% block content %}
<h2>{% trans "Create room" %}</h2>
<form method="post">{% csrf_token %}
{% block form_details %}
{% bootstrap_form form %}
{% endblock %}
<div class="float-end">
<button type="submit" name="save_action" class="save btn btn-secondary" value="save_add_another">
{% fa6_icon "plus" 'fas' %} {% trans "Save and add another" %}
</button>
<button type="submit" name="save_action" class="save btn btn-secondary" value="save_continue">
{% fa6_icon "pen" 'fas' %} {% trans "Save and continue editing" %}
</button>
<button type="submit" name="save_action" class="save btn btn-primary" value="save">
{% fa6_icon "check" 'fas' %} {% trans "Save" %}
</button>
</div>
<a href="javascript:history.back()" class="btn btn-info">
{% fa6_icon "times" 'fas' %} {% trans "Cancel" %}
</a>
</form>
{% endblock %}
......@@ -10,6 +10,7 @@ import django.db
from django.apps import apps
from django.contrib import admin, messages
from django.db.models.functions import Now
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy, reverse
from django.utils.dateparse import parse_datetime
......@@ -21,7 +22,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 +589,25 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
return super().form_valid(form)
class RoomCreationView(AdminViewMixin, CreateView):
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')
elif self.request.POST['save_action'] == 'save_continue':
return reverse_lazy('admin:AKModel_room_change', kwargs={'object_id': self.room.pk})
else:
return reverse_lazy('admin:AKModel_room_changelist')
def form_valid(self, form):
self.room = form.save()
messages.success(self.request, _("Created Room '%(room)s'" % {'room': self.room}))
return HttpResponseRedirect(self.get_success_url())
class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
form_class = RoomBatchCreationForm
title = _("Import Rooms from CSV")
......@@ -611,17 +631,13 @@ class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1
try:
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,
......
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
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()
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-03-24 18:01+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"
"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
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:16
#, python-format
msgid "Created Room '%(room)s'"
msgstr "Raum '%(room)s' angelegt"
#: AKOnline/views.py:18
#, 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',
),
]
......@@ -4,11 +4,21 @@ 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
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 %}
# Create your views here.
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from AKModel.views import AdminViewMixin, RoomCreationView
from AKOnline.forms import RoomWithVirtualForm
class RoomCreationWithVirtualView(RoomCreationView):
form_class = RoomWithVirtualForm
template_name = 'admin/AKOnline/room_create_with_virtual.html'
def form_valid(self, form):
objects = form.save()
self.room = objects['room']
messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']}))
if objects['virtual'] is not None:
messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']}))
return HttpResponseRedirect(self.get_success_url())
......@@ -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 %}
......
......@@ -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 %}
......
This diff is collapsed.
......@@ -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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment