diff --git a/src/backoffice/forms.py b/src/backoffice/forms.py index f6dd0a0374f8560ef2445e18d51b9445c206064d..1ddb05345dd80e3d5d4153fdd30e7b7466e5d150 100644 --- a/src/backoffice/forms.py +++ b/src/backoffice/forms.py @@ -7,7 +7,7 @@ from django.contrib.auth.forms import UserCreationForm from django.utils.translation import gettext_lazy as _, gettext from django.core.validators import validate_slug -from core.integrations import BigBlueButton, Hangar, IntegrationError, WorkAdventure +from core.integrations import Hangar, IntegrationError, WorkAdventure from core.models import Application, Assembly, AssemblyMember, Event, PlatformUser, Room, RoomLink, ScheduleSource from core.utils import str2timedelta @@ -126,6 +126,11 @@ class AssemblyEditForm(forms.ModelForm): # call original .clean() which e.g. removes 'slug' from cleaned_data if that isn't a slug super().clean() + # ensure assembly is either a channel or an assembly + if self.cleaned_data.get('state_assembly') == Assembly.State.NONE and self.cleaned_data.get('state_channel') == Assembly.State.NONE: + self.add_error('state_assembly', _('Assembly__states_must_not_be_none')) + self.add_error('state_channel', _('Assembly__states_must_not_be_none')) + # slug must not already exist in the conference slug = self.cleaned_data.get('slug') if slug is not None and Assembly.objects.filter(conference=self.instance.conference, slug=slug).exclude(pk=self.instance.pk).exists(): @@ -275,10 +280,8 @@ class AssemblyCreateRoomBigBlueButtonForm(AssemblyCreateRoomForm): room.save() self.room_id = room.id - if not self.create_room_on_backend(request, BigBlueButton, room): - room.delete() - room = None - self.room_id = None + # not creating a room on the backend as rooms will be created on first join. + # If empty, they time out after some minutes anyways and therefore get recreated regularly. return room @@ -325,7 +328,7 @@ class CreateAssemblyEventForm(forms.ModelForm): super().__init__(*args, **kwargs) self._rooms = rooms self.fields['room'].queryset = rooms - self.fields['room'].label = _('Event__room') + self.fields['room'].label = _('Event__room') self.fields['schedule_start'].widget.attrs['placeholder'] = _('Event__schedule_start__placeholder') self.fields['schedule_duration'].widget.attrs['placeholder'] = _('Event__schedule_duration__placeholder') @@ -346,10 +349,11 @@ class CreateAssemblyEventForm(forms.ModelForm): class EditAssemblyRoomForm(forms.ModelForm): class Meta: model = Room - fields = ['name', 'description'] + fields = ['name', 'slug', 'description'] def __init__(self, with_capacity=False, *args, **kwargs): super().__init__(*args, **kwargs) + self.fields['slug'].disabled = True if self.instance.room_type in Room.BACKEND_ROOMTYPES: self.fields['backend_status'] = forms.CharField(initial=self.instance.get_backend_status_display(), disabled=True) if self.instance.room_type in Room.TYPES_WITH_CAPACITY: @@ -365,6 +369,22 @@ class EditAssemblyRoomForm(forms.ModelForm): if self.instance.room_type in [Room.RoomType.HANGAR]: self.fields['name'].disabled = True + def clean_name(self): + if Room.objects.filter(assembly=self.instance.assembly, name=self.cleaned_data['name']).exclude(pk=self.instance.pk).exists(): + raise ValidationError(_("Room-name-assembly-unique")) + + # update slug to be based on the new name iff the name changed. + if self.instance.name != self.cleaned_data['name']: + self.instance.name = self.cleaned_data['name'] + self.instance.generate_slug() + self.cleaned_data['slug'] = self.instance.slug + return self.cleaned_data['name'] + + def clean_slug(self): + # self.instance.slug contains the new slug that was computed by `clean_name`. + # We return this here so that it doesn't get overwritten again by the initial slug value because of `self.fields['slug'].disabled = True` + return self.instance.slug + def save(self, commit=False): obj = super().save(commit) diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 5041caea42ad52e13a04e3663999c48561ee68f1..ed050947c6b1200740614f04665a14ced2e75c39 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-12-15 18:52+0100\n" +"POT-Creation-Date: 2021-12-16 01:30+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" @@ -87,6 +87,9 @@ msgstr "Ein Event mit diesem Namen existiert bereits in dieser Assembly!" msgid "Room-bigbluebutton__activeroom" msgstr "Der Raum ist aktiv. BBB unterstützt keine Kapazitätsänderung im laufenden Betrieb :(" +msgid "Room-name-assembly-unique" +msgstr "Raumnamen dürfen innerhalb einer Assembly nicht wiederverwendet werden" + msgid "Room-hangar_backend_link" msgstr "Hangar-Zugriff" @@ -339,6 +342,36 @@ msgstr "Diese Veranstaltung wirklich löschen (und nicht vllt. nur unsichtbar ma msgid "Events" msgstr "" +msgid "walist_info_paginated" +msgstr "Zeige _START_ bis _END_ von _TOTAL_ Einträgen" + +msgid "walist_info_empty" +msgstr "Zeige 0 Einträge" + +msgid "walist_info_filtered" +msgstr "(gefiltert, insgesamt _MAX_ Einträge)" + +msgid "walist_paginate_menu" +msgstr "Zeige _MENU_ Einträge" + +msgid "walist_search" +msgstr "Suche:" + +msgid "walist_noentries" +msgstr "Keine Einträge." + +msgid "walist_paginate_first" +msgstr "Erste" + +msgid "walist_paginate_last" +msgstr "Letzte" + +msgid "walist_paginate_next" +msgstr "Nächste" + +msgid "walist_paginate_previous" +msgstr "Vorherige" + msgid "assemblylist_info_paginated" msgstr "Zeige _START_ bis _END_ von _TOTAL_ Einträgen" @@ -512,6 +545,9 @@ msgstr "Übersicht" msgid "nav_assemblies" msgstr "Assembly-Team" +msgid "nav_channels" +msgstr "Channels-Team" + msgid "nav_users" msgstr "Teilnehmer" @@ -540,7 +576,7 @@ msgid "nav_login" msgstr "Anmelden" msgid "backoffice__not_a_conference_member" -msgstr "Das Nutzerkonto, mit dem du gerade angemeldet bist, ist zur Zeit nicht mit einem Veranstaltungsticket verknüpft. Wenn Du gerade beim Aufbau hilfst, ist das kein Problem. Denk aber bitte daran, rechtzeitig ein Ticket zu kaufen und dies mit deinem Nutzerkonto zu verknüpfen. Ansonsten kannst Du mit diesem Nutzerkonto nicht an der Veranstaltung teilnehmen." +msgstr "Das Nutzerkonto, mit dem du gerade angemeldet bist, ist zur Zeit nicht mit einem Veranstaltungsticket verknüpft. Wenn Du gerade beim Aufbau hilfst, ist das kein Problem. Denk aber bitte daran, rechtzeitig ein Ticket zu kaufen und dies kurz vor Veranstaltungsbeginn mit deinem Nutzerkonto zu verknüpfen. Ansonsten kannst Du mit diesem Nutzerkonto nicht an der Veranstaltung teilnehmen." msgid "Conference" msgstr "Konferenz" @@ -868,6 +904,21 @@ msgstr "" msgid "UserCommunicationChannel__address" msgstr "" +msgid "nav_channels_all" +msgstr "alle" + +msgid "nav_channels_accepted" +msgstr "akzeptiert" + +msgid "nav_channels_pending" +msgstr "wartend" + +msgid "nav_channels_planned" +msgstr "geplant" + +msgid "nav_channels_rejected" +msgstr "abgelehnt" + msgid "backoffice:assembly-organisational-data" msgstr "Organisatorisches" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 388c97f5cf41765fb6272a8a78e1a4b21174b71d..198043ad86366eb491bbf6a8a14ee5aa70ede5b2 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-12-15 18:52+0100\n" +"POT-Creation-Date: 2021-12-16 01:30+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" @@ -87,6 +87,9 @@ msgstr "An event with this name already exists in this assembly!" msgid "Room-bigbluebutton__activeroom" msgstr "The room is active. BBB doesn't support capacity changes in running rooms :(" +msgid "Room-name-assembly-unique" +msgstr "Room names must be Assembly-unique" + msgid "Room-hangar_backend_link" msgstr "Hangar Access" @@ -339,6 +342,36 @@ msgstr "You are about to delete this event. Sure you don't want to just hide it? msgid "Events" msgstr "" +msgid "walist_info_paginated" +msgstr "Showing _START_ to _END_ of _TOTAL_ entries" + +msgid "walist_info_empty" +msgstr "Showing 0 out of 0 entries" + +msgid "walist_info_filtered" +msgstr "(filtered from _MAX_ total entries)" + +msgid "walist_paginate_menu" +msgstr "Show _MENU_ entries" + +msgid "walist_search" +msgstr "Search:" + +msgid "walist_noentries" +msgstr "No entries." + +msgid "walist_paginate_first" +msgstr "First" + +msgid "walist_paginate_last" +msgstr "Last" + +msgid "walist_paginate_next" +msgstr "Next" + +msgid "walist_paginate_previous" +msgstr "Previous" + msgid "assemblylist_info_paginated" msgstr "Showing _START_ to _END_ of _TOTAL_ entries" @@ -513,6 +546,9 @@ msgstr "Home" msgid "nav_assemblies" msgstr "assemblies team" +msgid "nav_channels" +msgstr "channels team" + msgid "nav_users" msgstr "users" @@ -541,7 +577,7 @@ msgid "nav_login" msgstr "log in" msgid "backoffice__not_a_conference_member" -msgstr "The user account you are currently logged in with is not linked to an valid event ticket. If you are helping with the set-up, this is not a problem. But please remember to buy a ticket in time and link it to your user account. Otherwise you will not be able to participate in the event with this user account." +msgstr "The user account you are currently logged in with is not linked to an valid event ticket. If you are helping with the set-up, this is not a problem. But please remember to buy a ticket in time and link it to your user account. Otherwise you will not be able to participate in the event with this user account. Linking the ticket to the user account will be available directly before the event." msgid "Conference" msgstr "Conference" @@ -868,6 +904,21 @@ msgstr "" msgid "UserCommunicationChannel__address" msgstr "" +msgid "nav_channels_all" +msgstr "all" + +msgid "nav_channels_accepted" +msgstr "accepted" + +msgid "nav_channels_pending" +msgstr "pending" + +msgid "nav_channels_planned" +msgstr "planned" + +msgid "nav_channels_rejected" +msgstr "rejected" + msgid "backoffice:assembly-organisational-data" msgstr "Organisational Data" diff --git a/src/backoffice/static/backoffice.css b/src/backoffice/static/backoffice.css index 558101d25011d745cf2159380a43beaf63a599c7..2d06111ba1a3b0246cd896f39b116ed12f1785c3 100644 --- a/src/backoffice/static/backoffice.css +++ b/src/backoffice/static/backoffice.css @@ -2,6 +2,7 @@ display: flex; width: 100%; align-items: stretch; + min-height: calc(100vh - 56px); } .legal-footer { @@ -18,11 +19,10 @@ background: #7386D5; color: #fff; flex: 0 0 250px; - height: calc(100vh - 56px); } #sidebar .list-group-item, -#sidebar .list-group-item > a { +#sidebar .list-group-item > a { align-items: center; background: #7386D5; border: none; @@ -30,13 +30,13 @@ display: flex; } -#sidebar .list-group-item.active, +#sidebar .list-group-item.active, #sidebar .list-group-item.active a { color: #ff0; font-weight: bold; } -#sidebar .list-group-item.child, +#sidebar .list-group-item.child, #sidebar .list-group-item.child a { background: #6d7fcc; } @@ -62,3 +62,7 @@ #sidebar a.blocked { text-decoration: line-through; } + +.pb-10rem { + padding-bottom: 10rem; +} diff --git a/src/backoffice/templates/backoffice/assembly_auth.html b/src/backoffice/templates/backoffice/assembly_auth.html index d836514b443d81664107afd9c8b397309dbbcf78..f9315bf8498ae420f0098642704169a17e91217b 100644 --- a/src/backoffice/templates/backoffice/assembly_auth.html +++ b/src/backoffice/templates/backoffice/assembly_auth.html @@ -31,7 +31,7 @@ </div> </div> -<div class="row mt-3"> +<div class="row mt-3 pb-10rem"> <div class="col-md-12"> <div class="card border-primary"> <div class="card-header bg-primary text-white"> @@ -72,4 +72,4 @@ </form> </div> </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/backoffice/templates/backoffice/assembly_detail.html b/src/backoffice/templates/backoffice/assembly_detail.html index febcf535ad0872dcf014fb1b0237becbb06efadc..0d32c56d58111e03b13301d299d1eaaa7514ce71 100644 --- a/src/backoffice/templates/backoffice/assembly_detail.html +++ b/src/backoffice/templates/backoffice/assembly_detail.html @@ -72,7 +72,7 @@ {% if assembly.banner_image %} <div class="card-header">{{ assembly.banner_image_width }}x{{ assembly.banner_image_height }}px</div> <div class="card-body"> - <img src="{{ assembly.banner_image.url }}" alt="Banner Image"> + <img src="{{ assembly.banner_image.url }}" alt="Banner Image" class="img-fluid"> </div> {% else %} <div class="card-body">404 banner image :(</div> @@ -131,4 +131,4 @@ {% endif %} </div> </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/backoffice/templates/backoffice/assembly_edit.html b/src/backoffice/templates/backoffice/assembly_edit.html index 6196f384aaab18ff9f87f189447cd23eb7eb9db7..32810db10719d125de3892d7768fa677ec836bef 100644 --- a/src/backoffice/templates/backoffice/assembly_edit.html +++ b/src/backoffice/templates/backoffice/assembly_edit.html @@ -35,7 +35,7 @@ {% if not field.disabled %} {% render_field field class+="form-control" %} {% else %} - <input type="hidden" name="{{ field.name }}" value="{{ field.value }}">{{ field.value }} + <input type="hidden" name="{{ field.name }}" value="{{ field.value }}">{% render_field field class+="form-control" disabled="disabled" %} {% endif %} {% if field.help_text %} <small class="form-text text-muted">{{ field.help_text }}</small> diff --git a/src/backoffice/templates/backoffice/assembly_editlinks.html b/src/backoffice/templates/backoffice/assembly_editlinks.html index debb0553780072833fb7b90253584f6ae26a411e..79111c38111c971d7ffa1b07d48c2f5d955df1e7 100644 --- a/src/backoffice/templates/backoffice/assembly_editlinks.html +++ b/src/backoffice/templates/backoffice/assembly_editlinks.html @@ -37,7 +37,7 @@ </div> </div> -<div class="row"> +<div class="row pb-10rem"> <div class="col-md-12"> <div class="card mb-3"> <div class="card-header"> @@ -65,4 +65,4 @@ </div> {% endwith %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/backoffice/templates/backoffice/assembly_members.html b/src/backoffice/templates/backoffice/assembly_members.html index cabe71263e0aecece3fb46b7105276a58a46e1ed..34b6ebe7030cc60ee3446da9076b82685ba34a9f 100644 --- a/src/backoffice/templates/backoffice/assembly_members.html +++ b/src/backoffice/templates/backoffice/assembly_members.html @@ -9,7 +9,7 @@ {% block content %} {% include "backoffice/assembly_edit_header.html" %} -<div class="row pb-5"> +<div class="row pb-10rem"> <div class="col-md-12"> <div class="card border-default"> <div class="card-header bg-default"> @@ -51,10 +51,10 @@ <td>{% if member.show_public %}S{% else %}-{% endif %}</td> {% if can_manage %}<td> <a class="btn btn-secondary btn-sm" href="{% url 'backoffice:assembly-members-edit' pk=assembly.id uname=member.member.username %}">{% trans 'Assembly__members__edit' %}</a> - <input - type="submit" - class="btn btn-default btn-sm" - name="{% if member.show_public %}hide{% else %}show{% endif %}-{{ member.member.id }}" + <input + type="submit" + class="btn btn-default btn-sm" + name="{% if member.show_public %}hide{% else %}show{% endif %}-{{ member.member.id }}" value="{% if member.show_public %}{% trans 'Assembly__members__hide' %}{% else %}{% trans 'Assembly__members__show' %}{% endif %}"> <input type="submit" class="btn btn-default btn-sm" name="delete-{{ member.member.id }}" value="{% trans 'Assembly__members__delete' %}"> </td>{% endif %} diff --git a/src/backoffice/templates/backoffice/base.html b/src/backoffice/templates/backoffice/base.html index 2a2e93fe6cdf4919a9a231099b6168a6401f163c..a7d32af6d6c0d8a8f9b3c645ddc8768b6dac973b 100644 --- a/src/backoffice/templates/backoffice/base.html +++ b/src/backoffice/templates/backoffice/base.html @@ -29,6 +29,11 @@ <a class="nav-link" href="{% url 'backoffice:assemblies' %}">{% trans "nav_assemblies" %}{% if active_page == 'assemblies' %} <span class="sr-only">{{ activetab_srmarker }}</span>{% endif %}</a> </li> {% endif %} + {% if has_channel %} + <li class="nav-item{% if active_page == 'channels' %} active{% endif %}"> + <a class="nav-link" href="{% url 'backoffice:channels' %}">{% trans "nav_channels" %}{% if active_page == 'channels' %} <span class="sr-only">{{ activetab_srmarker }}</span>{% endif %}</a> + </li> + {% endif %} {% if has_users %} <li class="nav-item{% if active_page == 'users' %} active{% endif %}"> <a class="nav-link" href="{% url 'backoffice:users' %}">{% trans "nav_users" %}{% if active_page == 'users' %} <span class="sr-only">{{ activetab_srmarker }}</span>{% endif %}</a> @@ -107,18 +112,18 @@ {% endif %} {% for item in sidebar.items %} - <div + <div class="list-group-item list-group-action {% if item.active %} active{% endif %}" {% if item.active %}aria-current="true"{% endif %} {% if item.children %} - aria-expanded="{% if item.expanded %}true{% else %}false{% endif %}" - data-toggle="collapse" + aria-expanded="{% if item.expanded %}true{% else %}false{% endif %}" + data-toggle="collapse" data-target=".sidebar{{ forloop.counter }}" {% endif %} > {% if item.link %} - <a - href="{{ item.link }}" + <a + href="{{ item.link }}" class="d-block {{ item.class|join:' ' }}" onclick="event.stopPropagation()" > @@ -129,11 +134,11 @@ {% endif %} {% if item.children or item.count != '' or item.add_link %} {% if item.children %} - <a - href="#" - class="sidebar mr-auto {{ item.class|join:' ' }} dropdown-toggle" - aria-expanded="{% if item.expanded %}true{% else %}false{% endif %}" - data-toggle="collapse" + <a + href="#" + class="sidebar mr-auto {{ item.class|join:' ' }} dropdown-toggle" + aria-expanded="{% if item.expanded %}true{% else %}false{% endif %}" + data-toggle="collapse" data-target=".sidebar{{ forloop.counter }}"> {% if item.count != '' %}<span class="badge badge-light ml-1">{{ item.count }}</span>{% else %} {% endif %} </a> @@ -143,7 +148,7 @@ </span> {% endif %} {% if item.add_link %} - <a + <a href="{{ item.add_link }}" class="ml-1" onclick="event.stopPropagation()" @@ -153,18 +158,18 @@ {% endif %} {% endif %} </div> - + {% if item.children %} <div class="sidebar{{ forloop.counter }} collapse{% if item.expanded %} show{% endif %}"> {% for child in item.children %} - <a + <a class="list-group-item list-group-action justify-content-between child {% if child.active %} active{% endif %}" href="{{ child.link }}" > <div>{{ child.caption }}</div> {% if child.add_link %} - <a - href="{{ child.add_link }}" + <a + href="{{ child.add_link }}" class="ml-1" onclick="event.stopPropagation()" > @@ -179,7 +184,7 @@ </nav>{% endif %} {% endblock %} - <div class="container" style="margin-top: 1em;"> + <div class="container{% if user.is_authenticated and conferencemember == None %} pb-10rem{% endif %}" style="margin-top: 1em;"> {% if messages %} <div id="messages"> {% for message in messages %} @@ -208,6 +213,6 @@ <script src="{% static 'bootstrap4/bootstrap.min.js' %}"></script> {% block scripts %} {% endblock %} - <div class="legal-footer"><a href="https://legal.cccv.de" rel="nofollow noreferrer noopener" target="_blank">Impressum - Datenschutzerklärung</a></div> + <div class="legal-footer"><a href="https://legal.rc3.world/" rel="nofollow noreferrer noopener" target="_blank">Impressum - Datenschutzerklärung</a></div> </body> </html> diff --git a/src/backoffice/templates/backoffice/wa-map-list.html b/src/backoffice/templates/backoffice/wa-map-list.html index 32133b650766e63cc57e85ddcf511881554baaf4..b8048c296c2d565c0b243cdbf23f7c536d97d212 100644 --- a/src/backoffice/templates/backoffice/wa-map-list.html +++ b/src/backoffice/templates/backoffice/wa-map-list.html @@ -1,5 +1,46 @@ {% extends 'backoffice/base.html' %} {% load i18n %} +{% load static %} + +{% block htmlhead %} + <link rel="stylesheet" href="{% static 'datatables/datatables.min.css' %}"> +{% endblock %} + +{% block scripts %} + <script src="{% static 'datatables/datatables.min.js' %}"></script> + <script> + $(document).ready(function() { + $('#wa-maps').DataTable({ + pageLength: 100, + language: { + "decimal": "", + "emptyTable": "No data available in table", + "info": "{% trans 'walist_info_paginated' %}", + "infoEmpty": "{% trans 'walist_info_empty' %}", + "infoFiltered": "{% trans 'walist_info_filtered' %}", + "infoPostFix": "", + "thousands": " ", + "lengthMenu": "{% trans 'walist_paginate_menu' %}", + "loadingRecords": "LOADING ...", + "processing": "Processing...", + "search": "{% trans 'walist_search' %}", + "zeroRecords": "{% trans 'walist_noentries' %}", + "paginate": { + "first": "{% trans 'walist_paginate_first' %}", + "last": "{% trans 'walist_paginate_last' %}", + "next": "{% trans 'walist_paginate_next' %}", + "previous": "{% trans 'walist_paginate_previous' %}" + }, + "aria": { + "sortAscending": ": activate to sort column ascending", + "sortDescending": ": activate to sort column descending" + } + } + }); + }); + </script> +{% endblock %} + {% block content %} @@ -8,7 +49,7 @@ <div class="card-header"> WorkAdventure maps <span class="badge badge-primary">{{ object_list|length }}</span> </div> - <table class="card-body table table-hover table-sm table-striped"> + <table class="card-body table table-hover table-sm table-striped" id="wa-maps"> <thead> <tr> <th>Assembly</th> @@ -16,7 +57,6 @@ <th>State</th> <th>Occupants</th> <th>Capacity</th> - <th></th> </tr> </thead> <tbody> @@ -31,8 +71,6 @@ </td> <td>{{ obj.occupants|default:"-" }}</td> <td>{{ obj.capacity|default:"-" }}</td> - <td> - </td> </tr> {% endfor %} </tbody> diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 6420101d079918df378d657d781482c072fdac78..03a4aa2f64895c522881fbbab047a313881134c1 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -4,6 +4,7 @@ from .views import \ assemblies, \ assemblyteam, \ auth, \ + channelteam, \ events, \ misc, \ pages, \ @@ -45,6 +46,8 @@ urlpatterns = [ path('assemblies', assemblyteam.AssembliesView.as_view(), name='assemblies'), path('assemblies/list/<str:variant>', assemblyteam.AssembliesListsView.as_view(), name='assemblieslist'), + path('channels', channelteam.ChannelsView.as_view(), name='channels'), + path('channels/list/<str:variant>', channelteam.ChannelsListView.as_view(), name='channelslist'), path('assembly/create', assemblies.CreateAssemblyView.as_view(), name='assembly-create'), path('assembly/<uuid:pk>', assemblies.AssemblyView.as_view(), name='assembly'), diff --git a/src/backoffice/views/assemblies.py b/src/backoffice/views/assemblies.py index b9aa57829a528c426821bfcce379e195b0fcee96..96409a3c4f6d56c921367cd8d8189920ad18235c 100644 --- a/src/backoffice/views/assemblies.py +++ b/src/backoffice/views/assemblies.py @@ -90,12 +90,20 @@ class EditAssemblyView(AssemblyMixin, UpdateView): form['is_virtual'].disabled = True if not self.staff_access: - form['state_assembly'].disabled = True - form['state_channel'].disabled = True for x in AssemblyEditForm.Meta.staff_fields: if x in form.fields: del(form.fields[x]) + # disable assembly-related field editing for everyone except assembly team + if not self.assembly_staff_access: + form['state_assembly'].disabled = True + # but allow setting 'is_official' if only the channel team has access + form['is_official'].disabled = not (self.assembly.state_assembly == Assembly.State.NONE and self.channel_staff_access) + + # disable channel state editing for everyone except channel team + if not self.channel_staff_access: + form['state_channel'].disabled = True + return form def form_valid(self, form): @@ -128,6 +136,8 @@ class EditAssemblyView(AssemblyMixin, UpdateView): assembly.add_tag(tag, autocreate=True) if assembly.state_assembly == Assembly.State.PLANNED: assembly.state_assembly = Assembly.State.REGISTERED + if assembly.state_channel == Assembly.State.PLANNED: + assembly.state_channel = Assembly.State.REGISTERED if assembly.hierarchy == Assembly.Hierarchy.REGULAR: parent_id = form.cleaned_data.get('parent_id', '') if self.conference.support_clusters else None @@ -322,9 +332,11 @@ class AssemblyRoomView(AssemblyMixin, UpdateView): def get_form(self, *args, **kwargs): if self.object.room_type == Room.RoomType.WORKADVENTURE: + # keeping the form readonly. To make it writeable, do `return EditAssemblyRoomWorkAdventureForm(**self.get_form_kwargs())`` return EditAssemblyRoomWorkAdventureForm(instance=self.object) if self.object.room_type == Room.RoomType.HANGAR: + # keeping the form readonly. To make it writeable, do `return EditAssemblyRoomHangarForm(**self.get_form_kwargs())`` return EditAssemblyRoomHangarForm(instance=self.object) form = super().get_form(*args, **kwargs) diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py index 8258b870642375974c90c5872a4535a045daaab4..f6a56c552c02f2f5636588c6e515b982ecd4b6aa 100644 --- a/src/backoffice/views/assemblyteam.py +++ b/src/backoffice/views/assemblyteam.py @@ -23,6 +23,10 @@ logger = logging.getLogger(__name__) class AssemblyTeamMixin(ConferenceMixin): require_conference = True permission_required = ['core.assembly_team'] + active_page = 'assemblies' + base_view_name = 'backoffice:assemblies' + list_view_name = 'backoffice:assemblieslist' + sidebar_caption = _("nav_assemblies") MODES = { 'all': (Q(), _('nav_assemblies_all')), @@ -34,13 +38,14 @@ class AssemblyTeamMixin(ConferenceMixin): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['active_page'] = 'assemblies' + context['active_page'] = self.active_page + self.request.session['assembly_back'] = {'link': self.request.get_full_path(), 'title': str(self.sidebar_caption)} assemblies = [] lists = [] context['sidebar'] = { - 'title': _('nav_assemblies'), - # 'title_link': reverse('backoffice:assemblies'), + 'title': self.sidebar_caption, + # 'title_link': reverse(self.base_view_name), 'items': [ { 'caption': _('Assemblys'), @@ -57,24 +62,28 @@ class AssemblyTeamMixin(ConferenceMixin): for m, (q, t) in self.MODES.items(): assemblies.append({ + 'mode': m, 'caption': t, 'count': self.conference.assemblies.filter(q).count(), - 'link': reverse('backoffice:assemblies') + '?mode=' + m, + 'link': reverse(self.base_view_name) + '?mode=' + m, }) lists.append({ 'caption': format_lazy('{accepted}: slug+name', accepted=_('nav_assemblies_accepted')), - 'link': reverse('backoffice:assemblieslist', kwargs={'variant': 'slugname'}) + '?mode=accepted', + 'link': reverse(self.list_view_name, kwargs={'variant': 'slugname'}) + '?mode=accepted', + 'variant': 'slugname', }) lists.append({ 'caption': format_lazy('{accepted}: contacts', accepted=_('nav_assemblies_accepted')), - 'link': reverse('backoffice:assemblieslist', kwargs={'variant': 'assemblycontacts'}) + '?mode=accepted', + 'link': reverse(self.list_view_name, kwargs={'variant': 'assemblycontacts'}) + '?mode=accepted', + 'variant': 'assemblycontacts', }) lists.append({ 'caption': format_lazy('{accepted}: contact mails', accepted=_('nav_assemblies_accepted')), - 'link': reverse('backoffice:assemblieslist', kwargs={'variant': 'contactsmail'}) + '?mode=accepted', + 'link': reverse(self.list_view_name, kwargs={'variant': 'contactsmail'}) + '?mode=accepted', + 'variant': 'contactsmail', }) return context @@ -86,6 +95,13 @@ class AssembliesListMixin(AssemblyTeamMixin): def get_queryset(self, **kwargs): # not using .accessible_by_user() here as we're the Assembly Team anyway qs = Assembly.objects.filter(conference=self.conference) + if self.active_page == 'assemblies': + qs = qs.exclude(state_assembly=Assembly.State.NONE) + elif self.active_page == 'channels': + qs = qs.exclude(state_channel=Assembly.State.NONE) + else: + logging.warning('AssembliesListMixin: unexpected active_page="%s"', self.active_page) + qs = Assembly.objects.none() # mode selection mode = (self.request.POST if self.request.method == 'POST' else self.request.GET).get('mode', self.default_assemblies_mode) @@ -111,6 +127,12 @@ class AssembliesView(AssembliesListMixin, ListView): context['mode'] = self.assemblies_mode context['mode_display'] = self.MODES[self.assemblies_mode][1] + # activate current sidebar item + for sidebar_item in context['sidebar']['items'][0]['children']: + if sidebar_item['mode'] == self.assemblies_mode: + sidebar_item['active'] = True + break + for obj in context['object_list']: obj.user_can_edit = property(lambda x: x.user_can_manage(self.request.user, staff_can_manage=True)) @@ -211,6 +233,12 @@ class AssembliesListsView(AssembliesListMixin, View): ctx['fields'] = variant_fields ctx['data'] = data + # activate current sidebar item + for sidebar_item in ctx['sidebar']['items'][1]['children']: + if sidebar_item['variant'] == variant: + sidebar_item['active'] = True + break + ctx['download_available'] = len(data) > 0 ctx['download_default_delimiter'] = self.DEFAULT_DELIMITER_CHAR ctx['download_default_quotechar'] = self.DEFAULT_QUOTE_CHAR diff --git a/src/backoffice/views/channelteam.py b/src/backoffice/views/channelteam.py new file mode 100644 index 0000000000000000000000000000000000000000..e996f98db465b707ef74e50d7ba617d9b2e836d9 --- /dev/null +++ b/src/backoffice/views/channelteam.py @@ -0,0 +1,30 @@ +from django.utils.translation import gettext_lazy as _ +from django.db.models import Q + +from core.models.assemblies import Assembly + +from .assemblyteam import AssembliesView, AssembliesListsView + + +class ChannelsMixin: + """ sets options that configure the Assemblies views to work in Channels mode """ + MODES = { + 'all': (Q(), _('nav_channels_all')), + 'accepted': (Q(state_channel__in=Assembly.PUBLIC_STATES), _('nav_channels_accepted')), + 'pending': (Q(state_channel__in=[Assembly.State.REGISTERED]), _('nav_channels_pending')), + 'planned': (Q(state_channel__in=[Assembly.State.PLANNED]), _('nav_channels_planned')), + 'rejected': (Q(state_channel__in=[Assembly.State.REJECTED, Assembly.State.HIDDEN]), _('nav_channels_rejected')), + } + permission_required = ['core.channel_team'] + active_page = 'channels' + base_view_name = 'backoffice:channels' + list_view_name = 'backoffice:channelslist' + sidebar_caption = _("nav_channels") + + +class ChannelsView(ChannelsMixin, AssembliesView): + pass + + +class ChannelsListView(ChannelsMixin, AssembliesListsView): + pass diff --git a/src/backoffice/views/misc.py b/src/backoffice/views/misc.py index c5f350e8e3af640f6abe93318dfd9307c258b994..e995f2f290320ba05fef2fd8c0d77476c225b366 100644 --- a/src/backoffice/views/misc.py +++ b/src/backoffice/views/misc.py @@ -43,6 +43,9 @@ class IndexView(ConferenceMixin, View): def get(self, *args, **kwargs): if self.request.user.is_authenticated: myassemblies = list(Assembly.objects.associated_to_user(conference=self.conference, user=self.request.user)) + + # remove stored backlink for assembly pages from the session if it is set, so the backlink will go to the overview, which is the default + self.request.session.pop('assembly_back', None) else: myassemblies = None diff --git a/src/backoffice/views/mixins.py b/src/backoffice/views/mixins.py index eea0265eaffe894bf0c43a6b429de8bd53e2857e..a152c06b213f81fd2c8bfd491829fa4894b1c644 100644 --- a/src/backoffice/views/mixins.py +++ b/src/backoffice/views/mixins.py @@ -63,6 +63,10 @@ class ConferenceMixin(PermissionRequiredMixin): def is_assembly_team(self): return self.request.user.has_conference_staffpermission(self.conference, 'core.assembly_team') + @property + def is_channel_team(self): + return self.request.user.has_conference_staffpermission(self.conference, 'core.channel_team') + def dispatch(self, request, *args, **kwargs): if self.require_conference and self.conference is None: return redirect('conference_selection') @@ -91,6 +95,7 @@ class ConferenceMixin(PermissionRequiredMixin): if self.request.user.is_authenticated: context.update({ 'has_assemblies': self.is_assembly_team, + 'has_channel': self.is_channel_team, 'has_pages': self.request.user.has_conference_staffpermission(self.conference, 'core.static_pages'), 'has_users': self.request.user.has_conference_staffpermission(self.conference, 'core.platformusers'), 'has_schedules': self.request.user.has_conference_staffpermission(self.conference, 'core.scheduleadmin'), @@ -99,6 +104,7 @@ class ConferenceMixin(PermissionRequiredMixin): else: context.update({ 'has_assemblies': False, + 'has_channel': False, 'has_pages': False, 'has_users': False, 'has_schedules': False, @@ -126,9 +132,19 @@ class AssemblyMixin(LoginRequiredMixin, ConferenceMixin, SingleObjectMixin): super().__init__(*args, **kwargs) + # if _can_manage is set, user can edit assembly. This is a cache variable for the can_manage property self._can_manage = None + # if _staff_access is set, extra staff fields can be modified (internal comment, is_official) self._staff_access = False - self._staff_mode = None + # if _assembly_staff_access is set, the field state_assembly can be modified + self._assembly_staff_access = False + # if _channels_staff_access is set, the field state_channel can be modified + self._channels_staff_access = False + + # configures the staff warning + self._staff_mode = False + + # ensure self.object is present if not hasattr(self, 'object'): self.object = property(self._get_assembly) @@ -140,12 +156,19 @@ class AssemblyMixin(LoginRequiredMixin, ConferenceMixin, SingleObjectMixin): # check if it's the assembly team if self.request.user.has_conference_staffpermission(self.conference, 'assembly_team'): - self._staff_access = True + self._assembly_staff_access = True + self._staff_access = self._staff_access or assembly.state_assembly != Assembly.State.NONE + self._staff_mode = True + + # check if it's the channel team + if self.request.user.has_conference_staffpermission(self.conference, 'channel_team'): + self._channels_staff_access = True + self._staff_access = self._staff_access or assembly.state_channel != Assembly.State.NONE self._staff_mode = True # check if the current user is associated as a contact if assembly.has_user(self.request.user): - self._staff_mode = False + # don't set self._staff_mode = False here as this would prevent assembly team members to edit their own assemblies if not self._staff_access and \ assembly.state_assembly in [Assembly.State.NONE, Assembly.State.HIDDEN] and \ @@ -153,7 +176,7 @@ class AssemblyMixin(LoginRequiredMixin, ConferenceMixin, SingleObjectMixin): raise Assembly.DoesNotExist() # neither owner/manager nor assembly team? go away - elif not self._staff_access: + elif not self._assembly_staff_access and not self._channels_staff_access: raise PermissionDenied() self._assembly = assembly @@ -179,6 +202,14 @@ class AssemblyMixin(LoginRequiredMixin, ConferenceMixin, SingleObjectMixin): def staff_access(self): return self._staff_access + @property + def assembly_staff_access(self): + return self._assembly_staff_access + + @property + def channel_staff_access(self): + return self._channels_staff_access + @property def staff_mode(self): return self._staff_mode @@ -206,8 +237,12 @@ class AssemblyMixin(LoginRequiredMixin, ConferenceMixin, SingleObjectMixin): 'items': sidebar, } - if self.staff_mode: - context['sidebar']['back_link'] = {'link': reverse('backoffice:assemblies'), 'caption': _('nav_assemblies')} + # load backlink from the session if it set. Set by Assemblyteam / Channelsteam pages as there are a bunch of + # pages that will link to assembly views + if 'assembly_back' in self.request.session: + assembly_back = self.request.session['assembly_back'] + if 'link' in assembly_back and 'title' in assembly_back: + context['sidebar']['back_link'] = {'link': assembly_back['link'], 'caption': assembly_back['title']} organisation = [] sidebar.append({'caption': _('backoffice:assembly-organisational-data'), 'children': organisation}) diff --git a/src/backoffice/views/workadventure.py b/src/backoffice/views/workadventure.py index 05ef1363345f3a2c606f36fe022dd6b8a85f3914..c356b00db3871b2d2fa1c2284ff051af535350c7 100644 --- a/src/backoffice/views/workadventure.py +++ b/src/backoffice/views/workadventure.py @@ -10,6 +10,8 @@ from django.utils import timezone from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic.detail import SingleObjectMixin +from django.utils.translation import gettext_lazy as _ +from django.db.models import Q from core.integrations.workadventure import WorkAdventureIntegration from core.models.rooms import Room @@ -26,35 +28,54 @@ MAX_ROWS = 42 class WorkAdventureAdminMixin(LoginRequiredMixin, ConferenceMixin): permission_required = ['core.workadventure_admin'] + BACKEND_STATUS = { + 'all': (Q(room_type=Room.RoomType.WORKADVENTURE), 'all'), + 'new': (Q(room_type=Room.RoomType.WORKADVENTURE, backend_status=Room.BackendStatus.NEW), _('Room__backend_status-new')), + 'setup': (Q(room_type=Room.RoomType.WORKADVENTURE, backend_status=Room.BackendStatus.SETUP), _('Room__backend_status-setup')), + 'error': (Q(room_type=Room.RoomType.WORKADVENTURE, backend_status=Room.BackendStatus.ERROR), _('Room__backend_status-error')), + 'active': (Q(room_type=Room.RoomType.WORKADVENTURE, backend_status=Room.BackendStatus.ACTIVE), _('Room__backend_status-active')), + 'inactive': (Q(room_type=Room.RoomType.WORKADVENTURE, backend_status=Room.BackendStatus.INACTIVE), _('Room__backend_status-inactive')), + 'full': (Q(room_type=Room.RoomType.WORKADVENTURE, backend_status=Room.BackendStatus.FULL), _('Room__backend_status-full')), + } + def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - sidebar = [] + maps = [] context['sidebar'] = { 'title': 'WorkAdventure', 'title_link': reverse('backoffice:workadventure'), - 'items': sidebar, + 'items': [ + { + 'caption': _('Maps'), + 'children': maps, + 'expanded': True, + }, + { + 'caption': _('Sessions'), + 'link': reverse('backoffice:workadventure-session-list'), + 'expanded': False, + 'count': self.conference.workadventure_sessions.count(), + } + ], } - sidebar.append({ - 'caption': 'Maps', - 'link': reverse('backoffice:workadventure-map-list'), - 'count': self.conference.rooms.filter(room_type=Room.RoomType.WORKADVENTURE).count(), - }) - - sidebar.append({ - 'caption': 'Sessions', - 'link': reverse('backoffice:workadventure-session-list'), - 'count': self.conference.workadventure_sessions.count(), - }) + for m, (q, t) in self.BACKEND_STATUS.items(): + maps.append({ + 'caption': t, + 'count': self.conference.rooms.filter(q).count(), + 'link': reverse('backoffice:workadventure-map-list') + '?mode=' + m, + }) # try to guess 'active' sidebar item - request_url = self.request.META.get('PATH_INFO') - for x in sidebar: + query_string = '' + if self.request.META.get('QUERY_STRING'): + query_string = '?' + self.request.META.get('QUERY_STRING') + request_url = self.request.META.get('PATH_INFO') + query_string + for x in context['sidebar']['items']: if 'link' in x and x['link'] == request_url: x['active'] = True x['expanded'] = True - if 'children' in x: for y in x.get('children') or []: if 'link' in y and y['link'] == request_url: @@ -99,6 +120,20 @@ class WorkAdventureSessionMixin(WorkAdventureAdminMixin): class MapsView(WorkAdventureMapMixin, ListView): template_name = 'backoffice/wa-map-list.html' + default_backend_status = 'all' + + def get_queryset(self, *args, **kwargs): + qs = super().get_queryset(*args, **kwargs).select_related('assembly').order_by('assembly__slug', 'slug') + + backend_status = (self.request.POST if self.request.method == 'POST' else self.request.GET).get('mode', self.default_backend_status) + if backend_status in self.BACKEND_STATUS: + qs = qs.filter(self.BACKEND_STATUS[backend_status][0]) + else: + messages.warning(self.request, f'unknown mode "{backend_status}", using "all"') + backend_status = 'all' + self.backend_status = backend_status + + return qs class MapView(WorkAdventureMapMixin, DetailView): diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 3dd97efba4264f33b02e344a0ca15d7ad5bf5e0a..7dad9e681166152bcb9e6862b7bb53cd76c48b52 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -255,6 +255,9 @@ msgstr "Die Assembly muss einen Typ haben (zumindest eins von physisch/virtuell/ msgid "Assembly__technical_user__must_be_assembly" msgstr "Der technische Benutzer muss vom Typ 'Assembly' sein." +msgid "Assembly__states_must_not_be_none" +msgstr "Assemblystate oder Channelstate muss gesetzt sein!" + msgid "Assembly__slug__is_forbidden" msgstr "Dieser Kurzname ist verboten!" @@ -381,6 +384,9 @@ msgstr "Art/Verwendung" msgid "ConferenceMember__permission-assembly_team" msgstr "Assembly-Team: alle Assemblies verwaltbar, auch noch nicht fertig angelegte und abgelehnte sind sichtbar" +msgid "ConferenceMember__permission-channel_team" +msgstr "Channel-Team: alle Assemblies verwaltbar, auch noch nicht fertig angelegte und abgelehnte sind sichtbar" + msgid "ConferenceMember__permission-static_pages" msgstr "Statische Seiten: Verwaltung von Info-Seiten" diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index b4a7655850a23dc0739330483e6856636b7832a9..62b73f72e1abfe58c9d8f43659f22b751b977d9f 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -255,6 +255,9 @@ msgstr "The assembly requires a type (at least one of physical/virtual/remote)." msgid "Assembly__technical_user__must_be_assembly" msgstr "The technical user must be of type 'assembly'." +msgid "Assembly__states_must_not_be_none" +msgstr "assembly state or channel state must be set!" + msgid "Assembly__slug__is_forbidden" msgstr "this short name is forbidden" @@ -379,7 +382,10 @@ msgid "BadgeToken__badge_class" msgstr "class" msgid "ConferenceMember__permission-assembly_team" -msgstr "assembly team: manage all assemblies, see also incomplete and rejected registrations" +msgstr "assemblies team: manage all assemblies, see also incomplete and rejected registrations" + +msgid "ConferenceMember__permission-channel_team" +msgstr "channel team: manage all assemblies, see also incomplete and rejected registrations" msgid "ConferenceMember__permission-static_pages" msgstr "static pages: manage information pages" diff --git a/src/core/migrations/0070_room_slug.py b/src/core/migrations/0070_room_slug.py new file mode 100644 index 0000000000000000000000000000000000000000..d689213bde1c8900dede8f62ac437890bdfbe45e --- /dev/null +++ b/src/core/migrations/0070_room_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.10 on 2021-12-15 20:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0069_PlatformUser_indices'), + ] + + operations = [ + migrations.AddField( + model_name='room', + name='slug', + field=models.SlugField(blank=True, default=''), + ), + ] diff --git a/src/core/migrations/0071_room_slug2.py b/src/core/migrations/0071_room_slug2.py new file mode 100644 index 0000000000000000000000000000000000000000..f2824d30ffa9c2099c1454ddbed65e6741a2112f --- /dev/null +++ b/src/core/migrations/0071_room_slug2.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.10 on 2021-12-15 20:28 + +from django.db import migrations +from django.utils.text import slugify +from uuid import uuid4 + + +def create_slug(Room, room, extension=""): + """ + recursive function to get a free slug based on the username and an optional extension string. + """ + + sluged = slugify(room.name + extension) + + exists = Room.objects.exclude(id=room.id).filter(slug=sluged).exists() + if exists: + extension = str(uuid4())[13:18] + return create_slug(Room, room, extension) + else: + return sluged + + +def do(apps, schema_editor): + """ makes room names assembly-unique and computes slugs for all rooms """ + Room = apps.get_model('core', 'Room') + for room in Room.objects.all(): + i = 1 + room_name = room.name + while Room.objects.filter(assembly=room.assembly, name=room.name).exclude(pk=room.pk).exists(): + room.name = room_name + str(i) + i += 1 + room.slug = create_slug(Room, room) + room.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0070_room_slug'), + ] + + operations = [ + migrations.RunPython(do, migrations.RunPython.noop, elidable=True) + ] diff --git a/src/core/migrations/0072_room_slug3.py b/src/core/migrations/0072_room_slug3.py new file mode 100644 index 0000000000000000000000000000000000000000..fa9cafbacadd613b1044adfef204275aea5b4f65 --- /dev/null +++ b/src/core/migrations/0072_room_slug3.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.10 on 2021-12-15 20:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0071_room_slug2'), + ] + + operations = [ + migrations.AlterField( + model_name='room', + name='slug', + field=models.SlugField(unique=True), + ), + migrations.AddConstraint( + model_name='room', + constraint=models.UniqueConstraint(fields=('assembly', 'name'), name='room_unique_name'), + ), + ] diff --git a/src/core/migrations/0073_alter_room_assembly.py b/src/core/migrations/0073_alter_room_assembly.py new file mode 100644 index 0000000000000000000000000000000000000000..7a4e14c1dc7ebaaa5a651a42a6c2c90405d1c815 --- /dev/null +++ b/src/core/migrations/0073_alter_room_assembly.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.10 on 2021-12-17 07:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0072_room_slug3'), + ] + + operations = [ + migrations.AlterField( + model_name='room', + name='assembly', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='core.assembly'), + ), + ] diff --git a/src/core/migrations/0074_channel_team_permission.py b/src/core/migrations/0074_channel_team_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..681c61803987ff4d6fc016d31e2400df43d333a1 --- /dev/null +++ b/src/core/migrations/0074_channel_team_permission.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.10 on 2021-12-16 23:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0073_alter_room_assembly'), + ] + + operations = [ + migrations.AlterModelOptions( + name='conferencemember', + options={'permissions': [('assembly_team', 'ConferenceMember__permission-assembly_team'), ('channel_team', 'ConferenceMember__permission-channel_team'), ('static_pages', 'ConferenceMember__permission-static_pages'), ('platformusers', 'Orga: Users List'), ('rename_platformuser', 'Orga: Rename User'), ('block_platformuser', 'ConferenceMember__permission-block_platformuser'), ('change_conferencemember__active_angel', 'ConferenceMember__permission-change_conferencemember__active_angel'), ('view_platformuser__guardian', 'ConferenceMember__permission-view_platformuser__guardian'), ('scheduleadmin', 'ConferenceMember__permission-scheduleadmin'), ('workadventure_admin', 'ConferenceMember__permission-workadventure_admin')]}, + ), + ] diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index c530ffa3f14d456dc7e665459c32b511b9f7a321..6756f2d2eb9bb86a10dac58d8a3a99450a25511d 100644 --- a/src/core/models/assemblies.py +++ b/src/core/models/assemblies.py @@ -374,7 +374,7 @@ class Assembly(TaggedItemMixin, models.Model): if not user.is_authenticated: return False - if staff_can_manage and user.has_conference_staffpermission(self.conference, 'assembly_team'): + if staff_can_manage and user.has_conference_staffpermission(self.conference, 'assembly_team', 'channel_team'): return True return self.members.filter(member=user, can_manage_assembly=True).exists() diff --git a/src/core/models/conference.py b/src/core/models/conference.py index 7766e1841b0fedb188075430c4913f0c118a0b51..f22452d8c9addd5da476104a42c2e0dc11b524f7 100644 --- a/src/core/models/conference.py +++ b/src/core/models/conference.py @@ -34,6 +34,9 @@ class ConferenceMember(models.Model): ('assembly_team', _('ConferenceMember__permission-assembly_team')), # See all assemblies, not only the accepted ones. + ('channel_team', _("ConferenceMember__permission-channel_team")), + # Channelteam, Assemblyteam for Channel-Type Assemblies (Assemblies that provide their own content) + ('static_pages', _('ConferenceMember__permission-static_pages')), # Access to static pages, can be further limited by configuring static_page_groups. diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py index eb7ecbabe95e83fc3347c1ee4d2b3571cfbfbbdd..6952c9c0d08abfe312467aee76777664263fff07 100644 --- a/src/core/models/rooms.py +++ b/src/core/models/rooms.py @@ -3,6 +3,7 @@ from uuid import uuid4 from django.core.exceptions import ValidationError from django.core.validators import URLValidator from django.db import models +from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from ..fields import ConferenceReference @@ -47,6 +48,7 @@ class Room(models.Model): class Meta: verbose_name = _('Room') verbose_name_plural = _('Rooms') + constraints = [models.UniqueConstraint(fields=['assembly', 'name'], name='room_unique_name')] class RoomType(models.TextChoices): LECTURE_HALL = 'lecturehall', _('Room__type-lecturehall') @@ -104,7 +106,7 @@ class Room(models.Model): id = models.UUIDField(default=uuid4, primary_key=True, editable=False) conference = ConferenceReference(related_name='rooms') - assembly = models.ForeignKey(Assembly, related_name='rooms', on_delete=models.CASCADE, blank=True, null=True) + assembly = models.ForeignKey(Assembly, related_name='rooms', on_delete=models.CASCADE) blocked = models.BooleanField(default=False, help_text=_('Room__blocked__help'), verbose_name=_('Room__blocked')) """The room has been blocked for viewing.""" @@ -112,6 +114,8 @@ class Room(models.Model): name = models.CharField(max_length=200) """Name of the room as it can be found in the assembly.""" + slug = models.SlugField(unique=True) + description = models.TextField( blank=True, null=True, help_text=_('Room__description__help'), @@ -198,6 +202,27 @@ class Room(models.Model): return 'unknown' + def __create_slug(self, extension=''): + """ + recursive function to generate a free room slug based on the room name + """ + sluged = slugify(self.name + extension) + + exists = False + if self.id: + exists = Room.objects.exclude(id=self.id).filter(slug=sluged).exists() + else: + exists = Room.objects.filter(slug=sluged).exists() + + if exists: + extension = str(uuid4())[13:18] + return self.__create_slug(extension) + else: + return sluged + + def generate_slug(self): + self.slug = self.__create_slug() + def save(self, *args, update_fields=None, **kwargs): if update_fields is None or 'description' in update_fields: if self.description is None: @@ -205,6 +230,9 @@ class Room(models.Model): else: self.description_html = render_markdown(self.description) + if not self.slug: + self.generate_slug() + return super().save(*args, update_fields=update_fields, **kwargs) @classmethod diff --git a/src/core/models/users.py b/src/core/models/users.py index e77f6dbe0be580c7ecb238c0b148d0c5456d4a39..bcf5d36543baf7894ff0279f4e8fa424e6339058 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -35,7 +35,7 @@ class PlatformUser(AbstractUser): verbose_name=_('PlatformUser__type')) uuid = models.UUIDField(default=uuid4, unique=True) - # can be changed to blank=False, null=False after updating legacy objects + slug = models.SlugField(blank=False, unique=True) # legal stuff accepted_speakersagreement = models.BooleanField( diff --git a/src/plainui/jinja2/plainui/components/calendar.html b/src/plainui/jinja2/plainui/components/calendar.html index cbfac1351119ffca449646e0fd62390049733b82..114cb989da8187f072a80ec65b8d55eacdd0feee 100644 --- a/src/plainui/jinja2/plainui/components/calendar.html +++ b/src/plainui/jinja2/plainui/components/calendar.html @@ -19,7 +19,7 @@ {% if public %} <h2 class="mx-1 my-0 p-1 bg-info text-white rc3-fahrplan__title">{{room.name}}</h2> {% else %} - <a class="text-decoration-none" href="{{ url('plainui:room', room_id=room.id) }}"> + <a class="text-decoration-none" href="{{ url('plainui:room', room_slug=room.slug) }}"> <h2 class="mx-1 my-0 p-1 bg-info text-white rc3-fahrplan__title">{{room.name}}</h2> </a> {% endif %} diff --git a/src/plainui/jinja2/plainui/components/event_info.html b/src/plainui/jinja2/plainui/components/event_info.html index 83a45a520419c97545f56efb19f34728d98b3e6f..7c2e3003d45934384133d502d1e0d8238e042c5f 100644 --- a/src/plainui/jinja2/plainui/components/event_info.html +++ b/src/plainui/jinja2/plainui/components/event_info.html @@ -53,7 +53,7 @@ <dt class="grid-list__item grid-list__item--title">{{ _("Room") }}</dt> <dd class="grid-list__item grid-list__item--text"> {% if event.room and event.room.name %} - <a href="{{ url('plainui:room', room_id=event.room.id) }}" title="{{ event.room.name }}" class=""> + <a href="{{ url('plainui:room', room_slug=event.room.slug) }}" title="{{ event.room.name }}" class=""> {{ event.room.name }} </a> {% else %} diff --git a/src/plainui/jinja2/plainui/components/list_events.html b/src/plainui/jinja2/plainui/components/list_events.html index 7cf8ee72463927c7bdad3b33825e51172f56e515..42de1b1993f10be474668d150d5988c75614b6f9 100644 --- a/src/plainui/jinja2/plainui/components/list_events.html +++ b/src/plainui/jinja2/plainui/components/list_events.html @@ -283,7 +283,7 @@ <p class="text-center my-6 flex-lg-grow">{{ _("no entry availaible")}}</p> {% endif %} {% if room.room.id %} - <a href="{{ url('plainui:room', room_id=room.room.id) }}" class="mt-3 mt-lg-auto btn btn-secondary btn-lg btn-block">{{ _("View next Events") }}</a> + <a href="{{ url('plainui:room', room_slug=room.room.slug) }}" class="mt-3 mt-lg-auto btn btn-secondary btn-lg btn-block">{{ _("View next Events") }}</a> {% endif %} </div> </div> diff --git a/src/plainui/jinja2/plainui/components/list_rooms.html b/src/plainui/jinja2/plainui/components/list_rooms.html index 2e51ebed7a3322f8fde50f70e90d8435d0041155..cf216a4d7f9fcb3e64d67e61ee668d28c01b6ba0 100644 --- a/src/plainui/jinja2/plainui/components/list_rooms.html +++ b/src/plainui/jinja2/plainui/components/list_rooms.html @@ -19,7 +19,7 @@ {%- endmacro %} {% macro list_el(room) -%} - {% set link = url('plainui:room', room_id=room.id) %} + {% set link = url('plainui:room', room_slug=room.slug) %} {% set color="secondary" %} <li class="mt-3 d-flex border border-{{ color }} p-2 align-items-center font-headings"> <span class="btn-icon-big text-white" title="{{ _("roomtype") ~ ': ' ~ _(room.room_type) }}"> diff --git a/src/plainui/jinja2/plainui/redeem_token.html b/src/plainui/jinja2/plainui/redeem_token.html index c0c52985a344f35383b3b154c55dd098bd80cb37..d6c36d82108c5a8489362db01a99d06adc3d1d97 100644 --- a/src/plainui/jinja2/plainui/redeem_token.html +++ b/src/plainui/jinja2/plainui/redeem_token.html @@ -75,6 +75,6 @@ </div> {%- endif %} <p class="alert alert-info my-8" role="alert"> - {{ _("User accounts from signup.c3assemblies.de and maschinenraum.rc3.world are imported and can be used to login.") }} + {{ _("User accounts from maschinenraum.rc3.world can be used to login.") }} </p> {% endblock %} diff --git a/src/plainui/jinja2/plainui/room.html b/src/plainui/jinja2/plainui/room.html index fff1157e18a805a9b4463ad95792d2a6141257fd..1a159ec6e1889bc6deb154650b248e081bccf48d 100644 --- a/src/plainui/jinja2/plainui/room.html +++ b/src/plainui/jinja2/plainui/room.html @@ -9,8 +9,8 @@ {% endblock %} {% block content %} {{ titleMacro.title(room.name, - share_url = url('plainui:room', room_id=room.id), - report_url = url('plainui:room', room_id=room.id)) }} + share_url = url('plainui:room', room_slug=room.slug), + report_url = url('plainui:room', room_slug=room.slug)) }} {% if room.assembly %} <p class="font-headings text-white">{{ _("Assembly") }} <a href="{{ url('plainui:assembly', assembly_slug=room.assembly.slug) }}">{{room.assembly.name}}</a></p> diff --git a/src/plainui/jinja2/plainui/rooms.html b/src/plainui/jinja2/plainui/rooms.html index 86472b43bde3992704a6f3565961d43d5a55a765..e21258179a67bd102330c1544eb01942fc1dbd57 100644 --- a/src/plainui/jinja2/plainui/rooms.html +++ b/src/plainui/jinja2/plainui/rooms.html @@ -7,7 +7,7 @@ <ul class="mt-8 mb-11"> {% for room in rooms %} - <li><a href="{{ url('plainui:room', room_id=room.id) }}">{{room.name}}</a></li> + <li><a href="{{ url('plainui:room', room_slug=room.slug) }}">{{room.name}}</a></li> {% endfor %} </ul> {% endblock %} diff --git a/src/plainui/locale/de/LC_MESSAGES/django.po b/src/plainui/locale/de/LC_MESSAGES/django.po index c19aa41902de049f3c357be5d9a76a1454e90374..69e720f0f8c2973c73faeb04b7634f05b55b764c 100644 --- a/src/plainui/locale/de/LC_MESSAGES/django.po +++ b/src/plainui/locale/de/LC_MESSAGES/django.po @@ -632,8 +632,8 @@ msgstr "Du hast bereits ein Konto? Hier anmelden" msgid "Add your Ticket to this existing Account" msgstr "Ihr Ticket zu diesem bestehenden Konto hinzufügen" -msgid "User accounts from signup.c3assemblies.de and maschinenraum.rc3.world are imported and can be used to login." -msgstr "Accounts die auf signup.c3assemblies.de und maschinenraum.rc3.world bereits angelegt wurden, können weiter verwendet werden." +msgid "User accounts from maschinenraum.rc3.world can be used to login." +msgstr "Accounts die auf maschinenraum.rc3.world bereits angelegt wurden, können weiter verwendet werden." msgid "Please go to the following page and choose a new password:" msgstr "Bitte gehen Sie auf die folgende Seite und wählen Sie ein neues Passwort:" @@ -802,28 +802,28 @@ msgid "Welcome to rC3" msgstr "Willkommen bei der rC3" msgid "What's Official?" -msgstr "Was ist der Fahrplan?" +msgstr "Was ist der Fahr­plan?" msgid "official description" msgstr "Die virtuellen Bühnen 1 und 2 nennen sich passend \"rC1\" and \"rC2\", natürlich darf \"rc3 Lounge\" nicht fehlen. Zusätzlich steuern 15 Community Bühnen ihr unabhängig kuratiertes Programm zu den freien Streams der dezentralisierten remote Chaos Experience bei." msgid "Explore Curated Events Button" -msgstr "Hauptprogramm entdecken" +msgstr "Haupt­pro­gramm entdecken" msgid "Congress Platform Title" -msgstr "Plattform & 2D-Welt" +msgstr "Platt­form & 2D-Welt" msgid "platform description" msgstr "Ihr seid angekommen! Von hier geht es mitten rein in die remote Chaos Experience: Findet Eure favorisierten Vorträge und Workshops, springt von den Assembly Seiten direkt in deren 2D Karten, erkundet die 2D-Welt oder trefft Euch mit Freunden im Videochat. Viel Spaß!" msgid "Community Title" -msgstr "Community Content" +msgstr "Comm­unity Content" msgid "community description" msgstr "Die Assemblies - das seid Ihr: All die verschiedenen Gruppen, die das Chaos bunt und entdeckenswert machen. Bei der rC3 gibt es weit über 300 Assemblies, die Talks, Workshops, 2D-Welten und Diskussionsrunden anbieten, und natürlich die beliebten Self-organized Sessions für all Eure spannenden Wissensgebiete." msgid "Explore Community Events" -msgstr "Community Content entdecken" +msgstr "Comm­unity Content entdecken" #, python-format msgid "%(conf)s - Login" diff --git a/src/plainui/locale/en/LC_MESSAGES/django.po b/src/plainui/locale/en/LC_MESSAGES/django.po index dc2adb06351c9c84fe77bfffbd6a2a36775161c2..c50d37db0fefd7eaefe5534afa24b33246267d11 100644 --- a/src/plainui/locale/en/LC_MESSAGES/django.po +++ b/src/plainui/locale/en/LC_MESSAGES/django.po @@ -630,7 +630,7 @@ msgstr "" msgid "Add your Ticket to this existing Account" msgstr "" -msgid "User accounts from signup.c3assemblies.de and maschinenraum.rc3.world are imported and can be used to login." +msgid "User accounts from maschinenraum.rc3.world can be used to login." msgstr "" msgid "Please go to the following page and choose a new password:" diff --git a/src/plainui/tests.py b/src/plainui/tests.py index ee630d359bc94e0c2bbb5b3a4fe6eb4ddc6aabac..bc062bcf2d1ed05e770f68895e8c0ee043619231 100644 --- a/src/plainui/tests.py +++ b/src/plainui/tests.py @@ -1700,8 +1700,33 @@ class ViewsTest(TestCase): self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['events']['rooms_with_events'], [(room, [{'type': 'event', 'event': event, 'minutes': 45.0}])]) + def test_RoomView(self): + now = timezone.now() + self.conf.start = now - timedelta(days=3) + self.conf.end = now + timedelta(days=3) + self.conf.save() + + assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state_assembly=Assembly.State.PLACED) + assembly.save() + room = Room(conference=self.conf, assembly=assembly, name='Room1') + room.save() + room_link = RoomLink(room=room, name='stream', link_type=RoomLink.LinkType.VIDEO) + room_link.save() + + self.assertNeedsLogin(reverse('plainui:room', kwargs={'room_slug': room.slug})) + resp = self.client.get(reverse('plainui:room', kwargs={'room_slug': room.slug})) + self.assertEqual(resp.context_data['conf'], self.conf) + self.assertEqual(resp.context_data['room'], room) + def test_RoomsView(self): - room = Room(conference=self.conf, name='Room1') + now = timezone.now() + self.conf.start = now - timedelta(days=3) + self.conf.end = now + timedelta(days=3) + self.conf.save() + + assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state_assembly=Assembly.State.PLACED) + assembly.save() + room = Room(conference=self.conf, assembly=assembly, name='Room1') room.save() room_link = RoomLink(room=room, name='stream', link_type=RoomLink.LinkType.VIDEO) room_link.save() diff --git a/src/plainui/urls.py b/src/plainui/urls.py index 8e62742435211f02adaea60da4a448dbddb536d1..d02fbe456477cbcc3fe51519f82d35c656f3d41c 100644 --- a/src/plainui/urls.py +++ b/src/plainui/urls.py @@ -47,7 +47,7 @@ urlpatterns = [ path('pm/del', views.PersonalMessageDeleteView.as_view(), name='personal_message_delete'), path('public_fahrplan', views.PublicFahrplanView.as_view(), name='public_fahrplan'), path('rooms', views.RoomsView.as_view(), name='rooms'), - path('room/<uuid:room_id>/', views.RoomView.as_view(), name='room'), + path('room/<slug:room_slug>/', views.RoomView.as_view(), name='room'), path('static/<slug:page_slug>/', views.StaticPageView.as_view(), name='static_page'), path('search', views.SearchView.as_view(), name='search'), path('event/<slug:event_slug>/', views.EventView.as_view(), name='event'), @@ -61,6 +61,7 @@ urlpatterns = [ path('assembly/<slug:assembly_slug>/', views.AssemblyView.as_view(), name='assembly'), path('assembly/<slug:assembly_slug>/sos/new', views.SelfOrganizedSessionEditView.as_view(), name='sos_new'), path('assembly/<slug:assembly_slug>/sos/<slug:event_slug>/', views.SelfOrganizedSessionEditView.as_view(), name='sos_edit'), + path('assembly/<slug:assembly_slug>/bbb/<slug:room_slug>/', views.AssemblyJoinBBB.as_view(), name='assembly_join_bbb'), path('world', views.WorldView.as_view(), name='world'), path('report', views.ReportContentView.as_view(), name='report_content'), path('user/<slug:user_slug>/', views.UserView.as_view(), name='user'), diff --git a/src/plainui/views.py b/src/plainui/views.py index 434f4bc38a33f1f06d5076af6f38703b61bc0520..fd925fe8e8c510e5637c0fb7d2abd0c254c8da5f 100644 --- a/src/plainui/views.py +++ b/src/plainui/views.py @@ -1408,11 +1408,26 @@ class PublicFahrplanView(ConferenceRequiredMixin, TemplateView): return context +class AssemblyJoinBBB(ConferenceRequiredMixin, View): + """ extra view to join BBB rooms from the Workadventure """ + def get(self, request, assembly_slug, room_slug): + room = get_object_or_404(Room.objects.conference_accessible(self.conf), assembly=assembly_slug, slug=room_slug) + if not room.room_type == Room.RoomType.BIGBLUEBUTTON: + raise Http404() + + try: + return HttpResponseRedirect(integrations.BigBlueButton.join_room(room, self.request.user, self.request.user.show_name)) + except integrations.IntegrationError as e: + messages.error(request, str(e)) + + return redirect(reverse('plainui:assembly', kwargs={'assembly_slug': assembly_slug})) + + class RoomView(ConferenceRequiredMixin, TemplateView): template_name = 'plainui/room.html' def get(self, request, *args, **kwargs): - self.room = get_object_or_404(Room.objects.conference_accessible(self.conf), id=self.kwargs['room_id']) + self.room = get_object_or_404(Room.objects.conference_accessible(self.conf), slug=self.kwargs['room_slug']) try: if self.room.room_type == Room.RoomType.BIGBLUEBUTTON: