diff --git a/src/api/tests/map.py b/src/api/tests/map.py index 41bf4fe15d9dc949121c452615c12c074e2adeef..173260088398caef19e5b4c324583c953efc061f 100644 --- a/src/api/tests/map.py +++ b/src/api/tests/map.py @@ -49,6 +49,7 @@ class MapTest(TestCase): # set POI on assembly and accept it self.assembly.location_point = Point(8.9, 52.9) + self.assembly.location_state = Assembly.LocationState.ROUGH self.assembly.state = Assembly.State.ACCEPTED self.assembly.save() ConferenceExportCache.objects.all().delete() @@ -60,7 +61,7 @@ class MapTest(TestCase): self.assertEqual(0, len(content['features'])) # change state to 'placed', now there should be a POI - self.assembly.state = Assembly.State.PLACED + self.assembly.location_state = Assembly.LocationState.FINAL self.assembly.save() ConferenceExportCache.objects.all().delete() resp = self.client.get(url) diff --git a/src/api/views/maps.py b/src/api/views/maps.py index 245e265d5bef40efad8a46341f33e99e81b244d3..77b9209605e3e6e19f90e07056ac0e5ce795a491 100644 --- a/src/api/views/maps.py +++ b/src/api/views/maps.py @@ -5,9 +5,10 @@ from rest_framework.response import Response from rest_framework.views import APIView from django.contrib.gis.gdal import CoordTransform, SpatialReference +from django.db.models import Q, QuerySet from core.models.assemblies import Assembly -from core.models.conference import ConferenceExportCache +from core.models.conference import Conference, ConferenceExportCache from core.models.map import MapPOI from api.permissions import IsConferenceService, IsSuperUser @@ -16,6 +17,13 @@ from api.views.mixins import ConferenceSlugMixin _cts = {} # cache of CoordTransforms (if needed) +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) + + def get_field_geojson(value, srid: int, ct_cache: dict | None = None): if value.srid != srid: if ct_cache is None: @@ -70,8 +78,7 @@ class AssembliesExportView(ConferenceSlugMixin, APIView, metaclass=abc.ABCMeta): geometry_field = None def get_queryset(self): - exportable_states = [*Assembly.PLACED_STATES, Assembly.State.HIDDEN] - return Assembly.objects.filter(conference=self.conference, state__in=exportable_states) + return get_exportable_assemblies(conference=self.conference) def get_geometry_field(self, obj): return getattr(obj, self.geometry_field) @@ -137,8 +144,7 @@ class C3NavExportView(ConferenceSlugMixin, APIView): data = [] - exportable_states = [*Assembly.PLACED_STATES, Assembly.State.HIDDEN] - qs = self.conference.assemblies.filter(state__in=exportable_states) + qs = get_exportable_assemblies(self.conference) if request.GET.get('all') != '1': qs = qs.exclude(location_data__point=None, location_data__boundaries=None) for assembly in qs.all(): # type: Assembly @@ -153,8 +159,11 @@ class C3NavExportView(ConferenceSlugMixin, APIView): 'description': {'de': assembly.description_de, 'en': assembly.description_en}, 'public_url': hub_absolute('plainui:assembly', assembly_slug=assembly.slug), 'parent_id': assembly.parent_id, - 'children': assembly.children.filter(state__in=exportable_states).values_list('slug', flat=True) if assembly.is_cluster else None, + 'children': get_exportable_assemblies(conference=self.conference).filter(parent=assembly).values_list('slug', flat=True) + if assembly.is_cluster + else None, '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(), } diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 08f86f2090567158ea086cd8f8469406a5e2a1ca..b3cc091661bc1c6d3fe776d10e327e0fec9fdb66 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -646,9 +646,6 @@ msgstr "verstecken" msgid "Assembly__edit__state_btn_rejected2registered" msgstr "doch in Erwägung ziehen" -msgid "Assembly__edit__state_btn_placed" -msgstr "platziert" - msgid "Assembly__edit__state_btn_arrived" msgstr "angekommen (eigene Aussage)" @@ -664,6 +661,30 @@ 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__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 "AssemblyTeam__lastnote" msgstr "letzte Notiz" @@ -695,20 +716,43 @@ msgstr "" msgid "Assembly__edit__position_inform_assembly" msgstr "Verwaltende der Assemblies informieren" -msgid "Assembly__edit__position_update" -msgstr "Position aktualisieren" +msgid "AssemblyTeam__edit_position-save" +msgstr "Position speichern im Status:" -msgid "Assembly__edit__position_recall" -msgstr "Veröffentlichung zurücknehmen" +msgid "AssemblyTeam__edit_position__tooltip-none" +msgstr "Als 'keine Position' behandeln, auch wenn ggf. bereits eine gespeichert ist - z.B. ein alter Standort." -msgid "Assembly__edit__position_publish" -msgstr "mit Position veröffentlichen" +# use translation from core +msgid "Assembly__location_state-none" +msgstr "" -msgid "Assembly__edit__position_save_unpublished" -msgstr "Position vormerken" +msgid "AssemblyTeam__edit_position__tooltip-draft" +msgstr "Position ist eine Idee / nur grober Entwurf (nur für Assembly-Orga sichtbar)." -msgid "Assembly__edit__position_save_unaccepted" -msgstr "Position für nicht akzeptierte Assembly speichern" +# use translation from core +msgid "Assembly__location_state-draft" +msgstr "" + +msgid "AssemblyTeam__edit_position__tooltip-rough" +msgstr "Werte als grobe Position speichern (nur im Maschinenraum für die Assembly und ggf. Habitat sichtbar)." + +# use translation from core +msgid "Assembly__location_state-rough" +msgstr "" + +msgid "AssemblyTeam__edit_position__tooltip-preview" +msgstr "Werte als grobe Position speichern (wird mit diesem Hinweis auch bereits veröffentlicht)." + +# use translation from core +msgid "Assembly__location_state-preview" +msgstr "" + +msgid "AssemblyTeam__edit_position__tooltip-final" +msgstr "Finale bzw. öffentliche Position speichern." + +# use translation from core +msgid "Assembly__location_state-final" +msgstr "" msgid "Assembly__edit__state" msgstr "Anmelde-Status der Assembly ändern" @@ -1916,14 +1960,8 @@ msgstr "Es gab einen Fehler beim Parsen der Position, bitte erneut probieren." msgid "Assembly__edit__position_missing_on_publish" msgstr "Veröffentlichung abgebrochen: keine Positionsdaten gefunden" -msgid "Assembly__edit__position_was_not_published" -msgstr "Die Assembly war gar nicht veröffentlicht?!" - -msgid "Assembly__edit__position_unknown_action" -msgstr "Unbekannte Aktion, Position wurde dennoch gespeichert." - -msgid "Assembly__edit__changed_position" -msgstr "Position der Assembly wurde aktualisiert" +msgid "AssemblyTeam__edit_position__success" +msgstr "Die Position wurde erfolgreich aktualisert." #, python-format msgid "Badge__create__successful %(badge)s" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index a07f265e18d6390bfa569a347137506f377137cb..750a6417b354d2be5a65f13c32b07b266e130182 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -646,9 +646,6 @@ msgstr "hide" msgid "Assembly__edit__state_btn_rejected2registered" msgstr "consider it" -msgid "Assembly__edit__state_btn_placed" -msgstr "placed" - msgid "Assembly__edit__state_btn_arrived" msgstr "arrived (self)" @@ -664,6 +661,30 @@ 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__position-preview" +msgstr "preview" + +msgid "AssemblyTeam__position-draft" +msgstr "draft" + +msgid "AssemblyTeam__position-internal" +msgstr "assembly" + +msgid "AssemblyTeam__position-none" +msgstr "none" + msgid "AssemblyTeam__lastnote" msgstr "latest note" @@ -695,20 +716,43 @@ msgstr "" msgid "Assembly__edit__position_inform_assembly" msgstr "inform the assembly's management" -msgid "Assembly__edit__position_update" -msgstr "update position" +msgid "AssemblyTeam__edit_position-save" +msgstr "save the position with this state:" + +msgid "AssemblyTeam__edit_position__tooltip-none" +msgstr "handle this assembly as 'no position', even if a location may be set (e.g. old one)" + +# use translation from core +msgid "Assembly__location_state-none" +msgstr "" + +msgid "AssemblyTeam__edit_position__tooltip-draft" +msgstr "draft position, assembly team only" + +# use translation from core +msgid "Assembly__location_state-draft" +msgstr "" + +msgid "AssemblyTeam__edit_position__tooltip-rough" +msgstr "rough position, will be visible here in the backoffice only (i.e. with assemblies/habitats, too, but not external services like e.g. c3nav)" + +# use translation from core +msgid "Assembly__location_state-rough" +msgstr "" -msgid "Assembly__edit__position_recall" -msgstr "recall the assembly" +msgid "AssemblyTeam__edit_position__tooltip-preview" +msgstr "rough position, will be shared with external services like e.g. c3nav" -msgid "Assembly__edit__position_publish" -msgstr "publish the assembly" +# use translation from core +msgid "Assembly__location_state-preview" +msgstr "change assembly's registration state" -msgid "Assembly__edit__position_save_unpublished" -msgstr "save location (w/o publish)" +msgid "AssemblyTeam__edit_position__tooltip-final" +msgstr "final position, will be shared publicly" -msgid "Assembly__edit__position_save_unaccepted" -msgstr "save location for yet-unaccepted assembly" +# use translation from core +msgid "Assembly__location_state-final" +msgstr "" msgid "Assembly__edit__state" msgstr "change assembly's registration state" @@ -1921,14 +1965,8 @@ msgstr "The given location data could not be parsed." msgid "Assembly__edit__position_missing_on_publish" msgstr "Could not publish the assembly as there was no location data present." -msgid "Assembly__edit__position_was_not_published" -msgstr "Could not recall the assembly: it was not published?!" - -msgid "Assembly__edit__position_unknown_action" -msgstr "Unknown action requested, location was updated nonetheless." - -msgid "Assembly__edit__changed_position" -msgstr "Location of the assembly was updated." +msgid "AssemblyTeam__edit_position__success" +msgstr "The assembly's position has been updated successfully." #, python-format msgid "Badge__create__successful %(badge)s" diff --git a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html index 96eec278f4c0d9161ec0ff381d6b216136dff0e6..64df76b5d2aec3a7236e8381157ee59a2c4e0878 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html +++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html @@ -147,15 +147,6 @@ {% else %} {% if assembly.is_public %} - {% if assembly.state != 'placed' %} - <li> - <a class="dropdown-item text-secondary" - role="button" - href="{% url 'backoffice:assemblyteam-editstate' pk=assembly.id %}?state=placed"> - {% trans "Assembly__edit__state_btn_placed" %} - </a> - </li> - {% endif %} {% if assembly.state != 'arrived' %} <li> <a class="dropdown-item text-info" @@ -224,22 +215,38 @@ <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 "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> POI: + <i class="bi bi-pin-map"></i> {% trans "AssemblyTeam__position__point" %}: </label> <br> <label for="pos_boundary"> - <i class="bi bi-map"></i> Boundary: + <i class="bi bi-map"></i> {% trans "AssemblyTeam__position__boundary" %} </label> </div> <div class="flex-grow-1 ps-3"> - {% if object.is_placed %} - <span id="pos_public" class="text-success"><i class="bi bi-check-square"></i> {% trans "yes" %}</span> + {% 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-warning"><i class="bi bi-x-square"></i> {% trans "no" %}</span> + <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 %} diff --git a/src/backoffice/templates/backoffice/assemblyteam_editposition.html b/src/backoffice/templates/backoffice/assemblyteam_editposition.html index 83a33fe28583df5aadb402e47c005630d99b474b..c12dde635e12dc0808157e425b0f82410d96a6da 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_editposition.html +++ b/src/backoffice/templates/backoffice/assemblyteam_editposition.html @@ -79,35 +79,44 @@ placeholder="(optional)" autocomplete="off"></textarea> - {% if assembly.is_placed %} + <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 btn-primary" - name="action" - value="save">{% trans "Assembly__edit__position_update" %}</button> - + 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 btn-danger" - name="action" - value="recall">{% trans "Assembly__edit__position_recall" %}</button> - - {% elif assembly.is_public %} + 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 btn-primary" - name="action" - value="publish">{% trans "Assembly__edit__position_publish" %}</button> - + 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 btn-secondary" - name="action" - value="save">{% trans "Assembly__edit__position_save_unpublished" %}</button> - - {% else %} + 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 btn-danger" - name="action" - value="save">{% trans "Assembly__edit__position_save_unaccepted" %}</button> - - {% endif %} + 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> + </div> </form> </div> </div> diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index b3ceb9af6149da55edbcf45447b6301293242469..c7f1201d865df555db5c553dcea617fb07a4d6c8 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -62,6 +62,7 @@ urlpatterns = [ path('wiki/page/<uuid:pk>/delete', wiki.PageDeleteView.as_view(), name='wiki-page-delete'), path('wiki/page/<uuid:pk>/delete-revision', wiki.PageRevisionDeleteView.as_view(), name='wiki-page-revision-delete'), path('assemblies', assemblyteam.AssembliesView.as_view(), name='assemblies'), + path('assemblies/floor.geojson', assemblies.AssemblyGeoJsonExportView.as_view(), name='assemblies-floor-geojson'), path('assemblies/list/<str:variant>', assemblyteam.AssembliesListsView.as_view(), name='assemblieslist'), path('assemblyteam/<uuid:pk>', assemblyteam.AssemblyView.as_view(), name='assemblyteam-detail'), path('assemblyteam/<uuid:pk>/state', assemblyteam.AssemblyEditStateView.as_view(), name='assemblyteam-editstate'), diff --git a/src/backoffice/views/assemblies/__init__.py b/src/backoffice/views/assemblies/__init__.py index 3cc45e5da6afb2dbc608425b9abb8200a6172e1c..718271192081cba034e6a4b6fb3de19dc18e5e13 100644 --- a/src/backoffice/views/assemblies/__init__.py +++ b/src/backoffice/views/assemblies/__init__.py @@ -13,6 +13,9 @@ from backoffice.views.assemblies.auth import ( AuthView, VouchersView, ) +from backoffice.views.assemblies.map import ( + AssemblyGeoJsonExportView, +) from backoffice.views.assemblies.members import ( MemberCreateView, MemberListView, @@ -31,6 +34,7 @@ __all__ = [ 'AssemblyChildrenUpdateView', 'AssemblyCreateView', 'AssemblyDetailView', + 'AssemblyGeoJsonExportView', 'AssemblyLinksUpdateView', 'AssemblyListView', 'AssemblyParentLeaveView', diff --git a/src/backoffice/views/assemblies/map.py b/src/backoffice/views/assemblies/map.py new file mode 100644 index 0000000000000000000000000000000000000000..27f563ae6d79d70a37e54edc9c314f7df97416c8 --- /dev/null +++ b/src/backoffice/views/assemblies/map.py @@ -0,0 +1,51 @@ +from django.http import HttpResponseBadRequest, JsonResponse +from django.views import View + +from core.models import Assembly + +from backoffice.views.mixins import ConferenceLoginRequiredMixin + + +def flip_coordinates(coordinates): + """Flips the coordinates' tuples' items as latitude/longitude in GeoJSON represents to Y/X instead of X/Y.""" + result = [] + for polygon in coordinates: + p = [] + for coord in polygon: + p.append((coord[1], coord[0])) + result.append(p) + return result + + +class AssemblyGeoJsonExportView(ConferenceLoginRequiredMixin, View): + def get(self, request): + floor = request.GET.get('floor') + if floor is None: + return HttpResponseBadRequest('Missing "floor" parameter.') + + features = [] + + qs = Assembly.objects.conference_accessible(self.conference, self.conferencemember).filter(location_floor__index=floor) + qs = qs.exclude(location_data__point=None, location_data__boundaries=None) + + for assembly in qs: + if coords := assembly.location_data.get('boundaries'): + feature = { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': flip_coordinates(coords), + }, + 'properties': { + 'name': assembly.name, + 'slug': assembly.slug, + 'published': assembly.is_placed, + }, + } + features.append(feature) + + result = { + 'type': 'FeatureCollection', + 'features': features, + } + return JsonResponse(result, content_type='application/vnd.geo+json', json_dumps_params={'indent': 2}) diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py index 40cedbb0c1e3dcd056f2440a6b533ef096204c1c..42b35b619d1917f846ade973a2ed31a343a8a043 100644 --- a/src/backoffice/views/assemblyteam.py +++ b/src/backoffice/views/assemblyteam.py @@ -566,7 +566,7 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): request = self.request assembly = self.assembly - action = request.POST.get('action', 'save') + location_state = request.POST.get('location_state', None) inform_assembly = request.POST.get('inform_assembly', None) is not None poi = request.POST.get('location_point', None) boundaries = request.POST.get('location_boundaries', None) @@ -574,9 +574,9 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): comment = request.POST.get('comment', '').strip() old_values = { - 'state': assembly.state, - 'location_point': assembly.get_location_point_xy() or '', - 'location_boundaries': assembly.get_location_boundaries_xy() or '', + 'location_state': assembly.location_state, + 'location_point': str(assembly.get_location_point_xy() or ''), + 'location_boundaries': str(assembly.get_location_boundaries_xy() or ''), 'location_floor': str(assembly.get_location_floor_index() or ''), } changes = {} @@ -621,34 +621,24 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): messages.error(request, gettext('Assembly__edit__position_error')) return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk) - if action == 'publish': - # if assembly.location_point is not None or assembly.location_boundaries is not None: - if assembly.location_data.get('point') or assembly.location_data.get('boundaries'): - assembly.state = Assembly.State.PLACED - changes['state'] = assembly.state - else: + 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) - elif action == 'recall': - if assembly.is_placed: - assembly.state = Assembly.State.ACCEPTED - changes['state'] = assembly.state - else: - messages.warning(request, gettext('Assembly__edit__position_was_not_published')) - - elif action != 'save': - messages.warning(request, gettext('Assembly__edit__position_unknown_action')) + assembly.location_state = location_state + changes['location_state'] = location_state - assembly.save(update_fields=['state', 'location_point', 'location_boundaries', 'location_floor', 'location_data']) + assembly.save(update_fields=['location_state', 'location_point', 'location_boundaries', 'location_floor', 'location_data']) # log the action - messages.success(request, gettext('Assembly__edit__changed_position')) + messages.success(request, gettext('AssemblyTeam__edit_position__success')) logger.info( - 'Assembly "%(assembly_slug)s" (%(assembly_pk)s): set POI to "%(poi)s" & boundaries to "%(boundaries)s" & floor to "%(floor)s" upon request by <%(user)s>.', # noqa:E501 + 'Assembly "%(assembly_slug)s" (%(assembly_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 { 'assembly_slug': assembly.slug, 'assembly_pk': assembly.pk, + 'location_state': location_state, 'floor': floor, 'poi': poi, 'boundaries': boundaries, @@ -662,18 +652,6 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): **{k: ActivityLogChange(old=old_values[k], new=v) for k, v in changes.items() if old_values[k] != v}, ) - if assembly.state != old_values['state']: - messages.success(request, gettext('Assembly__edit__changed_state')) - logger.info( - 'Assembly "%(assembly_slug)s" (%(assembly_pk)s): changed state to "%(state)s" upon request by <%(user)s>.', - { - 'assembly_slug': assembly.slug, - 'assembly_pk': assembly.pk, - 'state': assembly.state, - 'user': self.request.user.username, - }, - ) - # invalidate export caches where necessary ConferenceExportCache.signal_assembly_modification(conference=self.conference, assembly=assembly) diff --git a/src/core/admin.py b/src/core/admin.py index 216cefcfa5c5cd90120f02881de53950b16d0b83..c00e2f344b16a5603d58bd58a4a1ca6cb4c48b4d 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -378,9 +378,9 @@ class ActivityLogEntryInline(GenericTabularInline): class AssemblyAdmin(GISModelAdmin): - list_display = ['conference', 'parent', 'slug', 'name', 'state', 'is_official'] + list_display = ['conference', 'parent', 'slug', 'name', 'state', 'location_state', 'is_official'] list_display_links = ['slug', 'name'] - list_filter = ['conference', 'parent', 'state', 'is_official'] + list_filter = ['conference', 'parent', 'state', 'location_state', 'is_official'] readonly_fields = ['id', 'conference'] search_fields = ['slug', 'name', 'description'] inlines = [TagsInline, BadgeInline, AssemblyLinkInline, AssemblyMemberInline, ActivityLogEntryInline] @@ -407,7 +407,7 @@ class AssemblyAdmin(GISModelAdmin): ( 'Location', { - 'fields': ['assembly_location', 'location_point', 'location_boundaries'], + 'fields': ['location_state', 'assembly_location', 'location_point', 'location_boundaries', 'location_data'], }, ), ) diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index aee8a920424d3546646e892efd7a84b515d2d475..54605f17f955c3b137a75835b2e6c296b318595e 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -281,6 +281,21 @@ msgstr "Habitat" msgid "Assembly__hierarchy__cluster_restricted" msgstr "Habitat (nicht öffentlich)" +msgid "Assembly__location_state-none" +msgstr "nicht verfügbar" + +msgid "Assembly__location_state-draft" +msgstr "Entwurf/Idee (Orga only)" + +msgid "Assembly__location_state-rough" +msgstr "grob (intern)" + +msgid "Assembly__location_state-preview" +msgstr "grob (öffentliche Vorschau)" + +msgid "Assembly__location_state-final" +msgstr "final (öffentlich)" + msgid "Assembly__slug__help" msgstr "Name der Assembly für URLs, bestehend aus Buchstaben, Nummern, Unterstrichen und Bindestrichen" @@ -381,6 +396,12 @@ msgstr "öffentlich sichtbare E-Mail für Kontakt" msgid "Assembly__public_contact" msgstr "öffentlicher Kontakt" +msgid "Assembly__location_state__help" +msgstr "Qualität der ausgewählten Position auf der Karte" + +msgid "Assembly__location_state" +msgstr "Position (Karte) Status" + msgid "Assembly__location_floor__help" msgstr "Ebene, auf der sich die Assembly befindet" diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index b66a8888e0460b2825bbb7051250afd0b57cd999..d5c6b5e3d29d407e90a308af459a86bdf22ae8f1 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -281,6 +281,21 @@ msgstr "habitat" msgid "Assembly__hierarchy__cluster_restricted" msgstr "habitat (non-public)" +msgid "Assembly__location_state-none" +msgstr "not available" + +msgid "Assembly__location_state-draft" +msgstr "draft (orga only)" + +msgid "Assembly__location_state-rough" +msgstr "rough (internal)" + +msgid "Assembly__location_state-preview" +msgstr "rough (public preview)" + +msgid "Assembly__location_state-final" +msgstr "final (public)" + msgid "Assembly__slug__help" msgstr "short name of this assembly used in URLs, consisting of letters, numbers, underscores and hyphens" @@ -381,6 +396,12 @@ msgstr "publicly visible email for contact" msgid "Assembly__public_contact" msgstr "public contact" +msgid "Assembly__location_state__help" +msgstr "quality of the selected position, how to handle it?" + +msgid "Assembly__location_state" +msgstr "location state" + msgid "Assembly__location_floor__help" msgstr "floor, on which this assembly is placed" diff --git a/src/core/migrations/0172_assembly_location_state.py b/src/core/migrations/0172_assembly_location_state.py new file mode 100644 index 0000000000000000000000000000000000000000..1fc3d11690572d52ecf2c7539c9af149dccedfa5 --- /dev/null +++ b/src/core/migrations/0172_assembly_location_state.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.3 on 2024-12-23 11:13 + +from django.db import migrations, models + + +def setup_location_state(apps, schema_editor): + Assembly = apps.get_model("core", "Assembly") # type=core.models.Assembly + for a in Assembly.objects.all(): + if a.location_data: + if a.state in ['placed', 'arrived', 'confirmed']: # aka a.is_placed + a.location_state = 'final' if a.state == 'placed' else 'preview' + else: + a.location_state = 'draft' + else: + a.location_state = 'none' + a.save(update_fields=["location_state"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0171_remove_team_require_staff_team_visibility'), + ] + + operations = [ + migrations.AddField( + model_name='assembly', + 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='Assembly__location_state__help', max_length=20, verbose_name='Assembly__location_state'), + ), + migrations.RunPython(setup_location_state, migrations.RunPython.noop, elidable=True), + ] diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index 292948f540dcc0fbc5893ba0d4ba51281dde8625..8990771ec281287afde06b9e82804aa0fa103c52 100644 --- a/src/core/models/assemblies.py +++ b/src/core/models/assemblies.py @@ -98,7 +98,7 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel): HIDDEN = 'hidden', _('Assembly__state-hidden') ACCEPTED = 'accepted', _('Assembly__state-accepted') REJECTED = 'rejected', _('Assembly__state-rejected') - PLACED = 'placed', _('Assembly__state-placed') + PLACED = 'placed', _('Assembly__state-placed') # TODO: entfernen, obsolet mit location_state ARRIVED_SELF = 'arrived', _('Assembly__state-arrived_self') ARRIVED_CONFIRMED = 'confirmed', _('Assembly__state-arrived_confirmed') @@ -107,6 +107,18 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel): CLUSTER = 'cluster', _('Assembly__hierarchy__cluster') CLUSTER_RESTRICTED = 'clusterrestricted', _('Assembly__hierarchy__cluster_restricted') + class LocationState(models.TextChoices): + NONE = 'none', _('Assembly__location_state-none') + """No location set.""" + DRAFT = 'draft', _('Assembly__location_state-draft') + """Internal preview, i.e. assembly orga only.""" + ROUGH = 'rough', _('Assembly__location_state-rough') + """Rough location idea, hub-internal only.""" + PREVIEW = 'preview', _('Assembly__location_state-preview') + """Rough location idea, shared with integrations like c3nav.""" + FINAL = 'final', _('Assembly__location_state-final') + """Precise location data, shared with the world.""" + objects = AssemblyManager() logger = logging.getLogger(__name__) @@ -201,6 +213,13 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel): related_query_name='assemblies_invited', ) + location_state = models.CharField( + max_length=20, + choices=LocationState.choices, + default=LocationState.NONE, + help_text=_('Assembly__location_state__help'), + verbose_name=_('Assembly__location_state'), + ) location_floor = models.ForeignKey( MapFloor, blank=True, @@ -266,11 +285,7 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel): State.ARRIVED_SELF.value, ] - PLACED_STATES = [ - State.PLACED.value, - State.ARRIVED_CONFIRMED.value, - State.ARRIVED_SELF.value, - ] + PLACED_LOCATION_STATES = [LocationState.PREVIEW, LocationState.FINAL] def __str__(self): return self.name @@ -361,7 +376,7 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel): @cached_property def is_placed(self): - return self.state in self.PLACED_STATES + return self.location_state in self.PLACED_LOCATION_STATES @cached_property def is_cluster(self): diff --git a/src/core/templates/core/map.html b/src/core/templates/core/map.html index 8b976f4f8a5247c74024a93fae0b92080490f931..931542903c5c69d6c7c5d20c3ad9fec1e3c13e7f 100644 --- a/src/core/templates/core/map.html +++ b/src/core/templates/core/map.html @@ -45,7 +45,15 @@ "{{ floor.name }}": floor{{ floor.code_slug }}, {% endfor %} }; - var overlays = {}; + + var assemblies = L.geoJSON().addTo(map); + + var overlays = { + assemblies, + }; + + loadAssemblies(initial_floor_idx); + const layerOptions = { collapsed: false, hideSingleBase: true, @@ -123,10 +131,61 @@ } {% endif %} + function styleAssemblies(feature) { + if (feature.properties.published) { + return { + color: "#CCFFCC", + weight: 2, + } + } else { + return { + color: "#FFCCCC", + weight: 1, + dashArray: "4 4", + dashOffset: "0", + } + } + } + + function handleAssemblyFeature (feature, layer) { + layer.bindTooltip( + '<b>' + feature.properties.slug + '</b><br>' + + feature.properties.name + + (feature.properties.published ? '' : '<br>NOT PUBLISHED'), + {permanent: false, opacity: 0.75}, + ) + } + + function loadAssemblies(activeFloor) { + map.removeLayer(assemblies); + //overlays = overlays.filter(item => item !== assemblies); // overlays.remove(assemblies); + + var url = "{% url "backoffice:assemblies-floor-geojson" %}?floor=" + activeFloor + fetch(url).then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + console.log('got GeoJSON for assemblies on floor', activeFloor, '=', data); + assemblies = L.geoJSON(data, { + onEachFeature: handleAssemblyFeature, + style: styleAssemblies, + }).addTo(map); + console.log('new assemblies layer:', assemblies); + overlays.push(assemblies); + + boundariesLayer.bringToFront(); + poiLayer.bringToFront(); + }); + } + function updateFloor(idx) { activeFloor = idx; const floor_elem = document.getElementById('{{ floor_id }}'); floor_elem.value = idx != null ? idx.toString() : ""; + loadAssemblies(activeFloor); } function updatePoiPosition(latLng) {