diff --git a/src/api/views/maps.py b/src/api/views/maps.py index 77b9209605e3e6e19f90e07056ac0e5ce795a491..b84b3288c0acc0f7bda16e48bebfb5f1d5fd6b9e 100644 --- a/src/api/views/maps.py +++ b/src/api/views/maps.py @@ -17,11 +17,13 @@ from api.views.mixins import ConferenceSlugMixin _cts = {} # cache of CoordTransforms (if needed) +q_publishable_location_states = Q(location_state__in=[Assembly.LocationState.PREVIEW, Assembly.LocationState.FINAL]) + + def get_exportable_assemblies(conference: Conference) -> QuerySet[Assembly]: """Fetches all assemblies in the given conference with publish-able location.""" exportable_states = [*Assembly.PUBLIC_STATES, Assembly.State.HIDDEN] - q_placed_assemblies = Q(location_state__in=[Assembly.LocationState.PREVIEW, Assembly.LocationState.FINAL]) - return Assembly.objects.filter(conference=conference, state__in=exportable_states).filter(q_placed_assemblies) + return Assembly.objects.filter(conference=conference, state__in=exportable_states).filter(q_publishable_location_states) def get_field_geojson(value, srid: int, ct_cache: dict | None = None): @@ -157,15 +159,17 @@ class C3NavExportView(ConferenceSlugMixin, APIView): 'name': assembly.name, 'is_official': assembly.is_official, 'description': {'de': assembly.description_de, 'en': assembly.description_en}, - 'public_url': hub_absolute('plainui:assembly', assembly_slug=assembly.slug), + 'public_url': hub_absolute('plainui:assembly', assembly_slug=assembly.slug, i18n=False), 'parent_id': assembly.parent_id, 'children': get_exportable_assemblies(conference=self.conference).filter(parent=assembly).values_list('slug', flat=True) if assembly.is_cluster else None, + 'rooms': assembly.rooms.filter(q_publishable_location_states).exclude(blocked=True).values_list('slug', flat=True), 'floor': assembly.get_location_floor_index(), 'is_preview': assembly.location_state != Assembly.LocationState.FINAL, 'location': loc_data.get('point'), # assembly.get_location_point_xy(), 'polygons': loc_data.get('boundaries'), # assembly.get_location_boundaries_xy(), + 'tags': assembly.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True), } ) @@ -177,14 +181,34 @@ class C3NavExportView(ConferenceSlugMixin, APIView): 'slug': project.slug, 'name': project.name, 'description': {'de': project.description_de, 'en': project.description_en}, - 'public_url': hub_absolute('plainui:project', slug=project.slug), + 'public_url': hub_absolute('plainui:project', slug=project.slug, i18n=False), 'assembly_id': str(assembly.pk), 'floor': assembly.get_location_floor_index(), # TODO: allow project-specific location floor + 'is_preview': assembly.location_state != Assembly.LocationState.FINAL, 'location': loc_data.get('point'), # TODO: allow project-specific location point 'location_text': project.location, + 'tags': project.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True), } ) + for room in self.conference.rooms.filter(q_publishable_location_states).exclude(blocked=True): + data.append( + { + 'type': 'room', + 'id': str(room.pk), + 'name': room.name, + 'is_official': room.is_official, + 'in_public_fahrplan': room.is_public_fahrplan, + 'description': {'de': room.description_de, 'en': room.description_en}, + 'public_url': hub_absolute('plainui:room', slug=room.slug, i18n=False), + 'is_preview': room.location_state != Assembly.LocationState.FINAL, + 'floor': room.get_location_floor_index(), + 'location': room.get_location_point_xy(), + 'polygons': room.get_location_boundaries_xy(), + 'tags': room.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True), + } + ) + for poi in self.conference.pois.filter(visible=True): # type: MapPOI data.append( { @@ -193,6 +217,7 @@ class C3NavExportView(ConferenceSlugMixin, APIView): 'name': {'de': poi.name_de, 'en': poi.name_en}, 'is_official': poi.is_official, 'description': {'de': poi.description_de, 'en': poi.description_en}, + 'is_preview': False, 'floor': poi.get_location_floor_index(), 'location': poi.get_location_point_xy(), } 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 64df76b5d2aec3a7236e8381157ee59a2c4e0878..2fc1e5149665d2246259c5cd0d37f40900f360b5 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html +++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html @@ -210,75 +210,8 @@ </div> {% if conference.support_assembly_physical %} - <div class="card mb-3"> - <div class="card-header">Position</div> - <div class="card-body d-flex flex-row"> - <div> - <label for="pos_public"> - <i class="bi bi-eye{% if not object.is_placed %}-slash{% endif %}"></i> {% trans "AssemblyTeam__position__status" %}: - </label> - <br> - <label for="pos_point"> - <i class="bi bi-pin-map"></i> {% trans "AssemblyTeam__position__point" %}: - </label> - <br> - <label for="pos_boundary"> - <i class="bi bi-map"></i> {% trans "AssemblyTeam__position__boundary" %} - </label> - </div> - <div class="flex-grow-1 ps-3"> - {% if object.location_state == object.LocationState.FINAL %} - <span id="pos_public" - class="text-success" - title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-final" %}</span> - {% elif object.location_state == object.LocationState.PREVIEW %} - <span id="pos_public" - class="text-success" - title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-preview" %}</span> - {% elif object.location_state == object.LocationState.DRAFT %} - <span id="pos_public" - class="text-warning" - title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-draft" %}</span> - {% elif object.location_state == object.LocationState.ROUGH %} - <span id="pos_public" - class="text-warning" - title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-internal" %}</span> - {% else %} - <span id="pos_public" - class="text-danger" - title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-none" %}</span> - {% endif %} - <br> - {% if object.location_data.point %} - <span id="pos_point" - class="text-success" - title="{{ object.location_data.point }}"> - <i class="bi bi-check-square"></i> {% trans "yes" %} - </span> - {% else %} - <span id="pos_poi" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span> - {% endif %} - <br> - {% if object.location_data.boundaries %} - <span id="pos_boundary" - class="text-success" - title="{{ object.location_data.boundaries }}"> - <i class="bi bi-check-square"></i> {% trans "yes" %} - </span> - {% else %} - <span id="pos_boundary" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span> - {% endif %} - </div> - <div> - <a role="button" - class="btn btn-sm btn-{% if assembly.is_public %}primary{% else %}secondary{% endif %}" - href="{% url 'backoffice:assemblyteam-editposition' pk=assembly.id %}"> - <i class="bi bi-pencil"></i> - {% trans "edit" %} - </a> - </div> - </div> - </div> + {% url 'backoffice:assemblyteam-editposition-assembly' pk=assembly.id as position_edit_url %} + {% include "backoffice/assemblyteam_position_info_card.html" %} {% endif %} {% if latest_note %} @@ -295,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.html b/src/backoffice/templates/backoffice/assemblyteam_editposition_assembly.html similarity index 55% rename from src/backoffice/templates/backoffice/assemblyteam_editposition.html rename to src/backoffice/templates/backoffice/assemblyteam_editposition_assembly.html index c12dde635e12dc0808157e425b0f82410d96a6da..6a60fd7c8aac2a8cac384321b6b44a4c6a57451a 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_editposition.html +++ b/src/backoffice/templates/backoffice/assemblyteam_editposition_assembly.html @@ -17,7 +17,7 @@ <div class="card mb-3"> <div class="card-header">{% trans "Assembly__edit__position" %}</div> <div class="card-body"> - <form action="{% url 'backoffice:assemblyteam-editposition' pk=assembly.id %}" + <form action="{% url 'backoffice:assemblyteam-editposition-assembly' pk=assembly.id %}" method="post"> {% csrf_token %} <input type="hidden" name="value" value="{{ new_value }}"> @@ -81,41 +81,7 @@ <label class="col-sm-2 col-form-label text-muted" for="buttons">{% trans "AssemblyTeam__edit_position-save" %}</label> <div class="btn-group" id="buttons"> - <button type="submit" - class="btn btn-sm {% if assembly.location_state == 'none' %}btn-primary{% else %}btn-outline-danger{% endif %}" - name="location_state" - value="none" - title="{% trans "AssemblyTeam__edit_position__tooltip-none" %}"> - {% trans "Assembly__location_state-none" %} - </button> - <button type="submit" - class="btn btn-sm {% if assembly.location_state == 'draft' %}btn-primary{% elif assembly.location_state == 'none' %}btn-outline-success{% elif assembly.location_state == 'rough' %}btn-outline-warning{% else %}btn-outline-danger{% endif %}" - name="location_state" - value="draft" - title="{% trans "AssemblyTeam__edit_position__tooltip-draft" %}"> - {% trans "Assembly__location_state-draft" %} - </button> - <button type="submit" - class="btn btn-sm {% if assembly.location_state == 'rough' %}btn-primary{% elif assembly.location_state == 'draft' %}btn-outline-success{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}" - name="location_state" - value="rough" - title="{% trans "AssemblyTeam__edit_position__tooltip-rough" %}"> - {% trans "Assembly__location_state-rough" %} - </button> - <button type="submit" - class="btn btn-sm {% if assembly.location_state == 'preview' %}btn-primary{% elif assembly.location_state == 'draft' %}btn-outline-warning{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}" - name="location_state" - value="preview" - title="{% trans "AssemblyTeam__edit_position__tooltip-preview" %}"> - {% trans "Assembly__location_state-preview" %} - </button> - <button type="submit" - class="btn btn-sm {% if assembly.location_state == 'final' %}btn-primary{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'draft' %}btn-outline-danger{% else %}btn-outline-primary{% endif %}" - name="location_state" - value="final" - title="{% trans "AssemblyTeam__edit_position__tooltip-final" %}"> - {% trans "Assembly__location_state-final" %} - </button> + {% include "backoffice/assemblyteam_editposition_statebuttons.html" with object=assembly %} </div> </form> </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/templates/backoffice/assemblyteam_editposition_statebuttons.html b/src/backoffice/templates/backoffice/assemblyteam_editposition_statebuttons.html new file mode 100644 index 0000000000000000000000000000000000000000..67f0664c614cb76312a5d3413c1368c06382e5e8 --- /dev/null +++ b/src/backoffice/templates/backoffice/assemblyteam_editposition_statebuttons.html @@ -0,0 +1,36 @@ +{% load i18n %} +<button type="submit" + class="btn btn-sm {% if object.location_state == 'none' %}btn-primary{% else %}btn-outline-danger{% endif %}" + name="location_state" + value="none" + title="{% trans "AssemblyTeam__edit_position__tooltip-none" %}"> + {% trans "Assembly__location_state-none" %} +</button> +<button type="submit" + class="btn btn-sm {% if object.location_state == 'draft' %}btn-primary{% elif object.location_state == 'none' %}btn-outline-success{% elif object.location_state == 'rough' %}btn-outline-warning{% else %}btn-outline-danger{% endif %}" + name="location_state" + value="draft" + title="{% trans "AssemblyTeam__edit_position__tooltip-draft" %}"> + {% trans "Assembly__location_state-draft" %} +</button> +<button type="submit" + class="btn btn-sm {% if object.location_state == 'rough' %}btn-primary{% elif object.location_state == 'draft' %}btn-outline-success{% elif object.location_state == 'none' %}btn-outline-danger{% elif object.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}" + name="location_state" + value="rough" + title="{% trans "AssemblyTeam__edit_position__tooltip-rough" %}"> + {% trans "Assembly__location_state-rough" %} +</button> +<button type="submit" + class="btn btn-sm {% if object.location_state == 'preview' %}btn-primary{% elif object.location_state == 'draft' %}btn-outline-warning{% elif object.location_state == 'none' %}btn-outline-danger{% elif object.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}" + name="location_state" + value="preview" + title="{% trans "AssemblyTeam__edit_position__tooltip-preview" %}"> + {% trans "Assembly__location_state-preview" %} +</button> +<button type="submit" + class="btn btn-sm {% if object.location_state == 'final' %}btn-primary{% elif object.location_state == 'none' %}btn-outline-danger{% elif object.location_state == 'draft' %}btn-outline-danger{% else %}btn-outline-primary{% endif %}" + name="location_state" + value="final" + title="{% trans "AssemblyTeam__edit_position__tooltip-final" %}"> + {% trans "Assembly__location_state-final" %} +</button> diff --git a/src/backoffice/templates/backoffice/assemblyteam_position_info_card.html b/src/backoffice/templates/backoffice/assemblyteam_position_info_card.html new file mode 100644 index 0000000000000000000000000000000000000000..00c5e80955387dd5654a9b22e706ec1ac1f908df --- /dev/null +++ b/src/backoffice/templates/backoffice/assemblyteam_position_info_card.html @@ -0,0 +1,72 @@ +{% load i18n %} +<div class="card mb-3"> + <div class="card-header">Position</div> + <div class="card-body d-flex flex-row"> + <div> + <label for="pos_public"> + <i class="bi bi-eye{% if not object.is_placed %}-slash{% endif %}"></i> {% trans "AssemblyTeam__position__status" %}: + </label> + <br> + <label for="pos_point"> + <i class="bi bi-pin-map"></i> {% trans "AssemblyTeam__position__point" %}: + </label> + <br> + <label for="pos_boundary"> + <i class="bi bi-map"></i> {% trans "AssemblyTeam__position__boundary" %} + </label> + </div> + <div class="flex-grow-1 ps-3"> + {% if object.location_state == object.LocationState.FINAL %} + <span id="pos_public" + class="text-success" + title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-final" %}</span> + {% elif object.location_state == object.LocationState.PREVIEW %} + <span id="pos_public" + class="text-success" + title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-preview" %}</span> + {% elif object.location_state == object.LocationState.DRAFT %} + <span id="pos_public" + class="text-warning" + title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-draft" %}</span> + {% elif object.location_state == object.LocationState.ROUGH %} + <span id="pos_public" + class="text-warning" + title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-internal" %}</span> + {% else %} + <span id="pos_public" + class="text-danger" + title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-none" %}</span> + {% endif %} + <br> + {% if object.location_data.point %} + <span id="pos_point" + class="text-success" + title="{{ object.location_data.point }}"> + <i class="bi bi-check-square"></i> {% trans "yes" %} + </span> + {% else %} + <span id="pos_poi" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span> + {% endif %} + <br> + {% if object.location_data.boundaries %} + <span id="pos_boundary" + class="text-success" + title="{{ object.location_data.boundaries }}"> + <i class="bi bi-check-square"></i> {% trans "yes" %} + </span> + {% else %} + <span id="pos_boundary" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span> + {% endif %} + </div> + {% if position_edit_url %} + <div> + <a role="button" + class="btn btn-sm btn-{% if assembly.is_public %}primary{% else %}secondary{% endif %}" + href="{{ position_edit_url }}"> + <i class="bi bi-pencil"></i> + {% trans "edit" %} + </a> + </div> + {% endif %} + </div> +</div> diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index c7f1201d865df555db5c553dcea617fb07a4d6c8..ea6b2f8a1d5051027259fef4fc9fa1189f4cc200 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -67,8 +67,9 @@ urlpatterns = [ path('assemblyteam/<uuid:pk>', assemblyteam.AssemblyView.as_view(), name='assemblyteam-detail'), path('assemblyteam/<uuid:pk>/state', assemblyteam.AssemblyEditStateView.as_view(), name='assemblyteam-editstate'), path('assemblyteam/<uuid:pk>/hierarchy', assemblyteam.AssemblyEditHierarchyView.as_view(), name='assemblyteam-edithierarchy'), - path('assemblyteam/<uuid:pk>/position', assemblyteam.AssemblyEditPlacementView.as_view(), name='assemblyteam-editposition'), + 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 e57fc7563cd05a3c025ed524316bf5c47efcbd60..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 @@ -639,12 +643,12 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): except ValueError: logger.exception('Failed to update position of assembly %s', assembly.pk) messages.error(request, gettext('Assembly__edit__position_error')) - return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk) + return redirect('backoffice:assemblyteam-editposition-assembly', pk=assembly.pk) if location_state and location_state != assembly.location_state: if location_state != Assembly.LocationState.NONE and not (assembly.location_data.get('point') or assembly.location_data.get('boundaries')): messages.warning(request, gettext('Assembly__edit__position_missing_on_publish')) - return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk) + return redirect('backoffice:assemblyteam-editposition-assembly', pk=assembly.pk) assembly.location_state = location_state changes['location_state'] = location_state @@ -680,7 +684,7 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): messages.warning(request, 'TODO: inform assembly') # we're done saving, redirect to edit view again - return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk) + return redirect('backoffice:assemblyteam-editposition-assembly', pk=assembly.pk) def get(self, *args, **kwargs): context = self.get_context_data() @@ -688,7 +692,123 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): loc_data = context['assembly'].location_data or {} context['map_location'] = loc_data.get('point') # context['assembly'].get_location_point_as_json() context['map_boundaries'] = loc_data.get('boundaries') # context['assembly'].get_location_boundaries_as_json() - return render(self.request, 'backoffice/assemblyteam_editposition.html', context) + 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): diff --git a/src/core/admin.py b/src/core/admin.py index c00e2f344b16a5603d58bd58a4a1ca6cb4c48b4d..6f45d562bfa6f2860dea297ee05701d75e0dc1e2 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -770,9 +770,9 @@ class RoomShareAdmin(admin.ModelAdmin): class RoomAdmin(admin.ModelAdmin): - list_display = ['conference', 'assembly', 'name', 'room_type', 'blocked', 'official_room_order', 'id'] + list_display = ['conference', 'assembly', 'name', 'room_type', 'location_state', 'blocked', 'official_room_order', 'id'] list_display_links = ['name'] - list_filter = ['conference', 'room_type', 'backend_status', 'blocked', 'is_official', 'is_public_fahrplan'] + list_filter = ['conference', 'room_type', 'backend_status', 'location_state', 'blocked', 'is_official', 'is_public_fahrplan'] save_as = True search_fields = ['assembly__name', 'name', 'slug', 'id'] inlines = [RoomLinkInline, RoomShareInline, TagsInline] @@ -792,6 +792,10 @@ class RoomAdmin(admin.ModelAdmin): 'Data', {'fields': ['name', 'room_type', 'capacity', 'occupants', 'description']}, ), + ( + 'Location', + {'fields': ['location_state', 'location_data']}, + ), ( 'Backend', {'fields': ['backend_link', 'backend_link_branch', 'backend_status', 'backend_data', 'director_data']}, diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 54605f17f955c3b137a75835b2e6c296b318595e..5f859b6dbd58c8f833ba236dd80a63ccaa6c2602 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -1763,6 +1763,18 @@ msgstr "Art des Raums" msgid "Room__type" msgstr "Typ" +msgid "Room__location_state__help" +msgstr "Qualität der ausgewählten Position auf der Karte" + +msgid "Room__location_state" +msgstr "Position (Karte) Status" + +msgid "Room__location_floor__help" +msgstr "Ebene, auf der sich der Raum befindet" + +msgid "Room__location_floor" +msgstr "Ebene" + msgid "Room__backend_link__help" msgstr "Referenz zu interner Quelle" diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index d5c6b5e3d29d407e90a308af459a86bdf22ae8f1..a2be9e62b7a53bc5502bfd2de34bbe866affb35d 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -1763,6 +1763,18 @@ msgstr "type of room" msgid "Room__type" msgstr "type" +msgid "Room__location_state__help" +msgstr "quality of the selected position, how to handle it?" + +msgid "Room__location_state" +msgstr "location state" + +msgid "Room__location_floor__help" +msgstr "floor, on which this room is placed" + +msgid "Room__location_floor" +msgstr "floor" + msgid "Room__backend_link__help" msgstr "reference to internal (backend) source" diff --git a/src/core/migrations/0174_room_location.py b/src/core/migrations/0174_room_location.py new file mode 100644 index 0000000000000000000000000000000000000000..6b96922c16b62a4f8018c676a3f13394a5201e77 --- /dev/null +++ b/src/core/migrations/0174_room_location.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.3 on 2024-12-26 13:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0173_lock_created_at'), + ] + + operations = [ + migrations.AddField( + model_name='room', + name='location_data', + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='room', + name='location_floor', + field=models.ForeignKey(blank=True, help_text='Room__location_floor__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rooms', to='core.mapfloor', verbose_name='Room__location_floor'), + ), + migrations.AddField( + model_name='room', + name='location_state', + field=models.CharField(choices=[('none', 'Assembly__location_state-none'), ('draft', 'Assembly__location_state-draft'), ('rough', 'Assembly__location_state-rough'), ('preview', 'Assembly__location_state-preview'), ('final', 'Assembly__location_state-final')], default='none', help_text='Room__location_state__help', max_length=20, verbose_name='Room__location_state'), + ), + ] diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py index cef75f46a77b2b89f46a675ac1a6e2c22217d303..5543aaa592aaf36ed90cf211528029b306de5188 100644 --- a/src/core/models/rooms.py +++ b/src/core/models/rooms.py @@ -1,3 +1,4 @@ +import json import uuid from datetime import datetime from pathlib import Path @@ -15,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 @@ -100,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') @@ -153,6 +155,27 @@ class Room(BackendMixin, models.Model): room_type = models.CharField(max_length=20, choices=RoomType.choices, help_text=_('Room__type__help'), verbose_name=_('Room__type')) """Style of the room.""" + location_state = models.CharField( + max_length=20, + choices=Assembly.LocationState.choices, + default=Assembly.LocationState.NONE, + help_text=_('Room__location_state__help'), + verbose_name=_('Room__location_state'), + ) + location_floor = models.ForeignKey( + 'MapFloor', + blank=True, + null=True, + related_name='rooms', + on_delete=models.PROTECT, + help_text=_('Room__location_floor__help'), + verbose_name=_('Room__location_floor'), + ) + location_data = models.JSONField( + blank=True, + null=True, + ) + backend_link = models.URLField(blank=True, null=True, help_text=_('Room__backend_link__help'), verbose_name=_('Room__backend_link')) """Reference to integration/source system. For e.g. WorkAdventure this is the collection JSON URL.""" @@ -301,6 +324,29 @@ class Room(BackendMixin, models.Model): def generate_slug(self): self.slug = self.__create_slug() + def get_location_floor_index(self) -> int | None: + return self.location_floor.index if self.location_floor else None + + def get_location_point_xy(self) -> tuple[int, int] | None: + if self.location_data is None: + return None + if p := self.location_data.get('point'): + return p[0], p[1] + return None + + def get_location_point_as_json(self) -> str: + return json.dumps(self.get_location_point_xy() or '') + + def get_location_boundaries_xy(self) -> list[list[tuple[int, int]]] | None: + if self.location_data is None: + return None + if b := self.location_data.get('boundaries'): + return b + return None + + def get_location_boundaries_as_json(self) -> str: + return json.dumps(self.get_location_boundaries_xy() or '') + def clean(self): if self.room_type == self.RoomType.PROJECT: raise ValidationError(_('Room__type_project_not_allowed'))