diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 89ce478d592bcb0675a2fe010a746fe16bb68a47..fe2cb0db938a0fca183f517592fe9de4423e6bf9 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -665,32 +665,26 @@ msgstr "Assembly bearbeiten" msgid "AssemblyTeam__message" msgstr "Nachricht senden" -msgid "AssemblyTeam__position__status" -msgstr "Status" - -msgid "AssemblyTeam__position__point" -msgstr "POI" - -msgid "AssemblyTeam__position__boundary" -msgstr "Umrandung" - -msgid "AssemblyTeam__position-final" -msgstr "final" +msgid "AssemblyTeam__lastnote" +msgstr "letzte Notiz" -msgid "AssemblyTeam__position-preview" -msgstr "Vorschau" +# use translation from core +msgid "Rooms" +msgstr "Raum" -msgid "AssemblyTeam__position-draft" -msgstr "Entwurf" +# use translation from core +msgid "Room__blocked__help" +msgstr "" -msgid "AssemblyTeam__position-internal" -msgstr "nur Assembly" +# use translation from core +msgid "Room__blocked" +msgstr "" -msgid "AssemblyTeam__position-none" -msgstr "nicht verf." +msgid "Room__location_point" +msgstr "Position (Karte)" -msgid "AssemblyTeam__lastnote" -msgstr "letzte Notiz" +msgid "Room__edit__position" +msgstr "Position des Raums bearbeiten" msgid "Assembly__edit__hierarchy" msgstr "Hierarchie/Art der Assembly ändern" @@ -723,6 +717,13 @@ msgstr "Verwaltende der Assemblies informieren" msgid "AssemblyTeam__edit_position-save" msgstr "Position speichern im Status:" +# use translation from core +msgid "Room__location_floor" +msgstr "" + +msgid "Room__location_boundaries" +msgstr "Bereiche" + msgid "AssemblyTeam__edit_position__tooltip-none" msgstr "Als 'keine Position' behandeln, auch wenn ggf. bereits eine gespeichert ist - z.B. ein alter Standort." @@ -779,6 +780,30 @@ msgstr "Nachrichten Empfänger" msgid "AssemblyTeam__message__send_button" msgstr "Nachricht versenden" +msgid "AssemblyTeam__position__status" +msgstr "Status" + +msgid "AssemblyTeam__position__point" +msgstr "POI" + +msgid "AssemblyTeam__position__boundary" +msgstr "Umrandung" + +msgid "AssemblyTeam__position-final" +msgstr "final" + +msgid "AssemblyTeam__position-preview" +msgstr "Vorschau" + +msgid "AssemblyTeam__position-draft" +msgstr "Entwurf" + +msgid "AssemblyTeam__position-internal" +msgstr "nur Assembly" + +msgid "AssemblyTeam__position-none" +msgstr "nicht verf." + msgid "nav_active_tab_sr_marker" msgstr "(ausgewählt)" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 82947e614461b264e496cb4e90c3f05195999048..3c10871739105cefd64e89e40e6bcb5a66ae58c0 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -665,32 +665,26 @@ msgstr "edit assembly" msgid "AssemblyTeam__message" msgstr "send a message" -msgid "AssemblyTeam__position__status" -msgstr "status" - -msgid "AssemblyTeam__position__point" -msgstr "POI" - -msgid "AssemblyTeam__position__boundary" -msgstr "boundary" - -msgid "AssemblyTeam__position-final" -msgstr "final" +msgid "AssemblyTeam__lastnote" +msgstr "latest note" -msgid "AssemblyTeam__position-preview" -msgstr "preview" +# use translation from core +msgid "Rooms" +msgstr "" -msgid "AssemblyTeam__position-draft" -msgstr "draft" +# use translation from core +msgid "Room__blocked__help" +msgstr "" -msgid "AssemblyTeam__position-internal" -msgstr "assembly" +# use translation from core +msgid "Room__blocked" +msgstr "" -msgid "AssemblyTeam__position-none" -msgstr "none" +msgid "Room__location_point" +msgstr "POI" -msgid "AssemblyTeam__lastnote" -msgstr "latest note" +msgid "Room__edit__position" +msgstr "placement of the room" msgid "Assembly__edit__hierarchy" msgstr "change assembly's categorization" @@ -723,6 +717,13 @@ msgstr "inform the assembly's management" msgid "AssemblyTeam__edit_position-save" msgstr "save the position with this state:" +# use translation from core +msgid "Room__location_floor" +msgstr "" + +msgid "Room__location_boundaries" +msgstr "boundaries" + msgid "AssemblyTeam__edit_position__tooltip-none" msgstr "handle this assembly as 'no position', even if a location may be set (e.g. old one)" @@ -779,6 +780,30 @@ msgstr "message recipients" msgid "AssemblyTeam__message__send_button" msgstr "Send the message" +msgid "AssemblyTeam__position__status" +msgstr "status" + +msgid "AssemblyTeam__position__point" +msgstr "POI" + +msgid "AssemblyTeam__position__boundary" +msgstr "boundary" + +msgid "AssemblyTeam__position-final" +msgstr "final" + +msgid "AssemblyTeam__position-preview" +msgstr "preview" + +msgid "AssemblyTeam__position-draft" +msgstr "draft" + +msgid "AssemblyTeam__position-internal" +msgstr "assembly" + +msgid "AssemblyTeam__position-none" +msgstr "none" + msgid "nav_active_tab_sr_marker" msgstr "(current)" diff --git a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html index 2630e8434dfba6dc443dac9747cc0c2c8fc8176b..2fc1e5149665d2246259c5cd0d37f40900f360b5 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html +++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html @@ -228,6 +228,32 @@ </div> </div> {% endif %} + + <div class="card"> + <div class="card-header">{% trans "Rooms" %}</div> + <div class="card-body"> + {% for room in assembly.rooms.all %} + <span class="fs-0 text-muted"> + {{ room.get_room_type_display }} + <abbr title="{{ room.name }}">{{ room.slug }}</abbr> + {% if room.blocked %} + <abbr class="text-danger" title="{% trans "Room__blocked__help" %}">{% trans "Room__blocked" %}</abbr> + {% endif %} + </span> + <br /> + <span class="text-{% if room.location_state == room.assembly.LocationState.NONE %}danger{% elif room.location_state == room.assembly.LocationState.FINAL %}success{% else %}warning{% endif %}"> + <abbr title="{% trans "Room__location_point" %}"><i class="bi bi-pin-map-fill"></i></abbr> + {{ room.get_location_state_display }} + </span> + <a class="btn btn-sm btn-outline-primary" + href="{% url "backoffice:assemblyteam-editposition-room" pk=room.pk %}" + title="{% trans "Room__edit__position" %}"><i class="bi bi-pencil"></i></a> + {% if not forloop.last %}<hr>{% endif %} + {% empty %} + <span class="text-muted">-/-</span> + {% endfor %} + </div> + </div> </div> </div> diff --git a/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html b/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html new file mode 100644 index 0000000000000000000000000000000000000000..7ed3706a6013a757730668d606cfd1b23c812423 --- /dev/null +++ b/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html @@ -0,0 +1,89 @@ +{% extends "backoffice/base.html" %} +{% load i18n %} +{% load static %} +{% load widget_tweaks %} + +{% block content %} + {% if form.errors %} + <div class="row"> + <div class="col-md-12"> + <div class="alert alert-danger">{{ form.errors }}</div> + </div> + </div> + {% endif %} + + <div class="row"> + <div class="col-md-12"> + <div class="card mb-3"> + <div class="card-header"> + <strong>{% trans "Room__edit__position" %}</strong>: + <code>{{ room.slug }}</code> + {{ room.name }} + </div> + <div class="card-body"> + <form action="{% url 'backoffice:assemblyteam-editposition-room' pk=room.id %}" + method="post"> + {% csrf_token %} + <input type="hidden" name="value" value="{{ new_value }}"> + + {% if room.blocked %} + <div class="alert alert-warning"> + <i class="bi bi-sign-do-not-enter"></i> {% trans "Room__blocked__help" %} + </div> + {% endif %} + + {% include "core/map.html" with map_config=conference.map_config.backoffice poi_id="poi" areas_id="areas" floor_id="floor" assembly=room.assembly %} + + <div class="d-{% if debug %}block{% else %}none{% endif %}"> + <div class="row mb-3"> + <label class="col-sm-2 col-form-label text-muted" for="floor">{% trans "Room__location_floor" %}</label> + <div class="col-sm-10"> + <input class="form-control" + readonly + type="text" + name="location_floor" + id="floor" + value="{{ room.get_location_floor_index|default:"" }}"> + </div> + </div> + <div class="row mb-3"> + <label class="col-sm-2 col-form-label text-muted" for="poi">{% trans "Room__location_point" %}</label> + <div class="col-sm-10"> + <input class="form-control" + readonly + type="text" + name="location_point" + id="poi" + value="{{ map_location|default:"" }}"> + </div> + </div> + <div class="row mb-3"> + <label class="col-sm-2 col-form-label text-muted" for="areas">{% trans "Room__location_boundaries" %}</label> + <div class="col-sm-10"> + <input class="form-control" + readonly + type="text" + name="location_boundaries" + id="areas" + value="{{ map_boundaries|default:"" }}"> + </div> + </div> + </div> + + <p>{% trans "AssemblyTeam__add_comment_field" %}</p> + <textarea name="comment" + class="form-control" + placeholder="(optional)" + autocomplete="off"></textarea> + + <label class="col-sm-2 col-form-label text-muted" for="buttons">{% trans "AssemblyTeam__edit_position-save" %}</label> + <div class="btn-group" id="buttons"> + {% include "backoffice/assemblyteam_editposition_statebuttons.html" with object=room %} + </div> + </form> + </div> + </div> + </div> + </div> + +{% endblock content %} diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 21e729cad7740ac985d2286622ba01bb12c0b254..ea6b2f8a1d5051027259fef4fc9fa1189f4cc200 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -69,6 +69,7 @@ urlpatterns = [ path('assemblyteam/<uuid:pk>/hierarchy', assemblyteam.AssemblyEditHierarchyView.as_view(), name='assemblyteam-edithierarchy'), path('assemblyteam/<uuid:pk>/position', assemblyteam.AssemblyEditPlacementView.as_view(), name='assemblyteam-editposition-assembly'), path('assemblyteam/<uuid:pk>/message', assemblyteam.AssemblyMessageView.as_view(), name='assemblyteam-message'), + path('assemblyteam/room/<uuid:pk>/position', assemblyteam.EditRoomPositionView.as_view(), name='assemblyteam-editposition-room'), path('assembly/create', assemblies.AssemblyCreateView.as_view(), name='assembly-create'), path('assembly/<uuid:pk>', assemblies.AssemblyDetailView.as_view(), name='assembly'), path('assembly/<uuid:pk>/edit', assemblies.AssemblyUpdateView.as_view(), name='assembly-edit'), diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py index 1d46577b0ba2c62d8a0de21318b1995d2f70550f..5695da9617ed089cc698d9ab4a0dd801adb9e879 100644 --- a/src/backoffice/views/assemblyteam.py +++ b/src/backoffice/views/assemblyteam.py @@ -162,7 +162,7 @@ class AssembliesListMixin(AssemblyTeamMixin, FilteredListView): return qs -def build_nav_from_assembly(assembly): +def build_nav_from_assembly(assembly, append_children: dict | None = None): def _build_nav_from_assembly(assmbly: Assembly, way_up: bool): # for cluster members, go way up to the top and trickle down if way_up and assmbly.parent: @@ -171,12 +171,16 @@ def build_nav_from_assembly(assembly): me = { 'caption': assmbly.slug, 'link': reverse('backoffice:assemblyteam-detail', kwargs={'pk': assmbly.pk}), - 'active': assembly.id == assmbly.id, + 'active': assembly.id == assmbly.id and not append_children, } if assmbly.is_cluster: me['children'] = [_build_nav_from_assembly(a, way_up=False) for a in assmbly.children.order_by('slug').all()] me['expanded'] = True + elif append_children: + # we are the last child (as we're not recursing further) + me['children'] = append_children + me['expanded'] = True return [me] if way_up else me @@ -691,6 +695,122 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): return render(self.request, 'backoffice/assemblyteam_editposition_assembly.html', context) +class EditRoomPositionView(AssemblyTeamMixin, View): + def dispatch(self, *args, **kwargs): + self.room = self.conference.rooms.get(pk=kwargs['pk']) + return super().dispatch(*args, **kwargs) + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + ctx['room'] = self.room + sidebar_room = { + 'caption': f'{self.room.get_room_type_display()} {self.room.slug}', + 'link': reverse('backoffice:assemblyteam-editposition-room', kwargs={'pk': self.room.pk}), + 'active': True, + } + ctx['sidebar'] = { + 'title': _('nav_assemblies'), + 'title_link': reverse('backoffice:assemblies'), + 'items': build_nav_from_assembly(self.room.assembly, append_children=[sidebar_room]), + } + return ctx + + def post(self, *args, **kwargs): + request = self.request + room = self.room + + location_state = request.POST.get('location_state', None) + poi = request.POST.get('location_point', None) + boundaries = request.POST.get('location_boundaries', None) + floor = request.POST.get('location_floor', None) + comment = request.POST.get('comment', '').strip() + + old_values = { + 'location_state': room.location_state, + 'location_point': str(room.get_location_point_xy() or ''), + 'location_boundaries': str(room.get_location_boundaries_xy() or ''), + 'location_floor': str(room.get_location_floor_index() or ''), + } + changes = {} + try: + room.location_data = room.location_data or {} + + if poi and poi != '""': + parsed = json.loads(poi) + assert isinstance(parsed, list) and len(parsed) == 2 + + room.location_data['point'] = parsed + changes['location_point'] = poi + else: + room.location_data['point'] = None + changes['location_point'] = '' + + if boundaries and boundaries != '""': + parsed = json.loads(boundaries) + assert isinstance(parsed, list) + + room.location_data['boundaries'] = parsed + changes['location_boundaries'] = boundaries + else: + room.location_data['boundaries'] = None + changes['location_boundaries'] = '' + + if floor and floor != '""': + parsed = int(floor) + + room.location_floor = self.conference.map_floors.filter(index=parsed).first() + changes['location_floor'] = str(room.get_location_floor_index() or '') + else: + room.location_floor = None + changes['location_floor'] = '' + + except ValueError: + logger.exception('Failed to update position of room %s', room.pk) + messages.error(request, gettext('Assembly__edit__position_error')) + return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk) + + if location_state and location_state != room.location_state: + if location_state != Assembly.LocationState.NONE and not (room.location_data.get('point') or room.location_data.get('boundaries')): + messages.warning(request, gettext('Assembly__edit__position_missing_on_publish')) + return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk) + + room.location_state = location_state + changes['location_state'] = location_state + + room.save(update_fields=['location_state', 'location_floor', 'location_data']) + + # log the action + messages.success(request, gettext('AssemblyTeam__edit_position__success')) + logger.info( + 'Room "%(room_slug)s" (%(room_pk)s): set location_state to "%(location_state)s, POI to "%(poi)s" & boundaries to "%(boundaries)s" & floor to "%(floor)s" upon request by <%(user)s>.', # noqa:E501 + { + 'room_slug': room.slug, + 'room_pk': room.pk, + 'location_state': location_state, + 'floor': floor, + 'poi': poi, + 'boundaries': boundaries, + 'user': self.request.user.username, + }, + ) + room.log_activity( + user=self.request.user, + kind=ActivityLogEntry.Kind.STAFF, + comment=comment if comment else None, + **{k: ActivityLogChange(old=old_values[k], new=v) for k, v in changes.items() if old_values[k] != v}, + ) + + # we're done saving, redirect to edit view again + return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk) + + def get(self, *args, **kwargs): + context = self.get_context_data() + context['uses_map'] = True + context['map_location'] = self.room.get_location_point_as_json() + context['map_boundaries'] = self.room.get_location_boundaries_as_json() + return render(self.request, 'backoffice/assemblyteam_editposition_room.html', context) + + class AssemblyMessageView(SingleAssemblyTeamMixin, FormView): template_name = 'backoffice/assemblyteam_message.html' form_class = AssemblyTeamMessageForm diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py index a0fdf814675cb8975719fedd00b62d88bca31869..5543aaa592aaf36ed90cf211528029b306de5188 100644 --- a/src/core/models/rooms.py +++ b/src/core/models/rooms.py @@ -16,6 +16,7 @@ from django.utils.translation import gettext_lazy as _ from core.fields import ConferenceReference from core.markdown import compile_translated_markdown_fields, store_relationships +from core.models.activitylog import ActivityLogMixin from core.models.assemblies import Assembly from core.models.base_managers import ConferenceManagerMixin from core.models.conference import Conference, ConferenceMember @@ -101,7 +102,7 @@ def get_banner_help_test() -> str: } -class Room(BackendMixin, models.Model): +class Room(BackendMixin, ActivityLogMixin, models.Model): class Meta: verbose_name = _('Room') verbose_name_plural = _('Rooms')