From 870c26e3356e2ea0c66d0ec51719b0a3096390ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 19 Dec 2024 17:26:50 +0100 Subject: [PATCH] doors now have todos, names and a cooler new UI --- src/c3nav/editor/forms.py | 40 +++++++++++++++- src/c3nav/editor/templates/editor/edit.html | 18 ++++++++ src/c3nav/editor/views/edit.py | 46 +++++++++++++++++-- .../migrations/0123_door_name_door_todo.py | 23 ++++++++++ src/c3nav/mapdata/models/geometry/level.py | 12 +++++ 5 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0123_door_name_door_todo.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 8f0e0fa1..83c5c9e1 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -3,7 +3,7 @@ import operator import os from functools import reduce from itertools import chain -from operator import attrgetter +from operator import attrgetter, itemgetter from django.conf import settings from django.core.cache import cache @@ -398,7 +398,7 @@ class EditorFormBase(I18nModelFormMixin, ModelForm): def create_editor_form(editor_model): possible_fields = [ - 'slug', 'name', 'title', 'title_plural', 'help_text', 'position_secret', 'icon', 'join_edges', + 'slug', 'name', 'title', 'title_plural', 'help_text', 'position_secret', 'icon', 'join_edges', 'todo', 'up_separate', 'bssid', 'main_point', 'external_url', 'external_url_label', 'hub_import_type', 'walk', 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name', 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside', @@ -493,3 +493,39 @@ class GraphEditorActionForm(Form): def clean_clicked_position(self): return GeometryField(geomtype='point').to_python(self.cleaned_data['clicked_position']) + + +class DoorGraphForm(Form): + def __init__(self, *args, request, spaces, nodes, edges, **kwargs): + self.request = request + self.edges = edges + self.restrictions = {a.pk: a for a in AccessRestriction.qs_for_request(request)} + super().__init__(*args, **kwargs) + + choices = ( + (-1, '--- no edge'), + (0, '--- edge without restriction'), + *((pk, restriction.title) for pk, restriction in self.restrictions.items()) + ) + + for (from_node, to_node), edge in sorted(edges.items(), key=itemgetter(0)): + self.fields[f'edge_{from_node}_{to_node}'] = ChoiceField( + choices=choices, + label=f'{spaces[nodes[from_node].space_id]} → {spaces[nodes[to_node].space_id]}', + initial=-1 if edge is None else (edge.access_restriction_id or 0), + ) + + def save(self): + for (from_node, to_node), edge in self.edges.items(): + cleaned_value = int(self.cleaned_data[f'edge_{from_node}_{to_node}']) + if edge is None: + if cleaned_value == -1: + continue + GraphEdge.objects.create(from_node_id=from_node, to_node_id=to_node, + access_restriction_id=(cleaned_value or None)) + else: + if cleaned_value == -1: + edge.delete() + elif edge.access_restriction_id != (cleaned_value or None): + edge.access_restriction_id = (cleaned_value or None) + edge.save() diff --git a/src/c3nav/editor/templates/editor/edit.html b/src/c3nav/editor/templates/editor/edit.html index 54bae714..ff9b8c7c 100644 --- a/src/c3nav/editor/templates/editor/edit.html +++ b/src/c3nav/editor/templates/editor/edit.html @@ -23,6 +23,24 @@
{% csrf_token %} {% bootstrap_form form %} + {% if door %} +
+

{% trans 'Connecting spaces:' %}

+ + {% bootstrap_form door.form %} +
+ {% endif %} {% buttons %} {% if can_edit %} diff --git a/src/c3nav/editor/views/edit.py b/src/c3nav/editor/views/edit.py index 0ac6232f..75e5c1d2 100644 --- a/src/c3nav/editor/views/edit.py +++ b/src/c3nav/editor/views/edit.py @@ -6,7 +6,7 @@ from django.apps import apps from django.conf import settings from django.contrib import messages from django.core.cache import cache -from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, PermissionDenied +from django.core.exceptions import FieldDoesNotExist, PermissionDenied from django.db import IntegrityError, models from django.db.models import Q from django.http import Http404, HttpResponse @@ -14,14 +14,16 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import etag +from shapely import LineString -from c3nav.editor.forms import GraphEdgeSettingsForm, GraphEditorActionForm, get_editor_form +from c3nav.editor.forms import GraphEdgeSettingsForm, GraphEditorActionForm, get_editor_form, DoorGraphForm from c3nav.editor.utils import DefaultEditUtils, LevelChildEditUtils, SpaceChildEditUtils from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse, APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse, editor_etag_func, sidebar_view, accesses_mapdata) -from c3nav.mapdata.models import Level, Space, LocationGroupCategory, GraphNode, GraphEdge +from c3nav.mapdata.models import Level, Space, LocationGroupCategory, GraphNode, GraphEdge, Door from c3nav.mapdata.models.access import AccessPermission, AccessRestriction, AccessRestrictionGroup +from c3nav.mapdata.utils.geometry import unwrap_geom from c3nav.mapdata.utils.user import can_access_editor @@ -284,6 +286,31 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e return APIHybridLoginRequiredResponse(next=request.path_info, login_url='editor.login', level='info', message=_('You need to log in to create Beacon Measurements.')) + graph_form = None + if model == Door and not new: + door_geom = unwrap_geom(obj.geometry) + spaces = { + subspace.pk: subspace + for subspace in Space.qs_for_request(request).filter(level=level).select_related('access_restriction') + if subspace.geometry.intersects(door_geom) + } + edges = [ + edge + for edge in GraphEdge.qs_for_request(request).filter( + from_node__space__in=spaces, to_node__space__in=spaces + ).select_related('from_node', 'to_node') + if LineString((edge.from_node.coords, edge.to_node.coords)).intersects(door_geom) + ] + nodes = {} + for edge in edges: + nodes[edge.from_node.pk] = edge.from_node + nodes[edge.to_node.pk] = edge.to_node + edges = {(edge.from_node.pk, edge.to_node.pk): edge for edge in edges} + for from_node, to_node in tuple(edges): + if (to_node, from_node) not in edges: + edges[(to_node, from_node)] = None + ctx["door"] = {"spaces": spaces} + error = None delete = getattr(request, 'is_delete', None) if request.method == 'POST' or (not new and delete): @@ -333,7 +360,12 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e form = get_editor_form(model)(instance=model() if new else obj, data=data, is_json=json_body is not None, request=request, space_id=space_id, geometry_editable=edit_utils.can_access_child_base_mapdata) - if form.is_valid(): + + if "door" in ctx: + graph_form = DoorGraphForm(request=request, spaces=spaces, nodes=nodes, edges=edges, data=data) + ctx["door"]["form"] = graph_form + + if form.is_valid() and (graph_form is None or graph_form.is_valid()): # Update/create objects obj = form.save(commit=False) @@ -358,7 +390,8 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e for slug in form.remove_redirect_slugs: obj.redirects.filter(slug=slug).delete() - + if graph_form is not None: + graph_form.save() form.save_m2m() return APIHybridMessageRedirectResponse( level='success', @@ -371,6 +404,9 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e else: form = get_editor_form(model)(instance=obj, request=request, space_id=space_id, geometry_editable=edit_utils.can_access_child_base_mapdata) + if "door" in ctx: + graph_form = DoorGraphForm(request=request, spaces=spaces, nodes=nodes, edges=edges) + ctx["door"]["form"] = graph_form ctx.update({ 'form': form, diff --git a/src/c3nav/mapdata/migrations/0123_door_name_door_todo.py b/src/c3nav/mapdata/migrations/0123_door_name_door_todo.py new file mode 100644 index 00000000..6ca8e5c4 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0123_door_name_door_todo.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.8 on 2024-12-19 16:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0122_locationgroup_external_url_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='door', + name='name', + field=models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='Name'), + ), + migrations.AddField( + model_name='door', + name='todo', + field=models.BooleanField(default=False, verbose_name='todo'), + ), + ] diff --git a/src/c3nav/mapdata/models/geometry/level.py b/src/c3nav/mapdata/models/geometry/level.py index 02229b6e..ee9ea271 100644 --- a/src/c3nav/mapdata/models/geometry/level.py +++ b/src/c3nav/mapdata/models/geometry/level.py @@ -151,12 +151,24 @@ class Door(LevelGeometryMixin, AccessRestrictionMixin, models.Model): A connection between two spaces """ geometry = GeometryField('polygon') + name = models.CharField(_('Name'), unique=True, max_length=50, blank=True, null=True) + todo = models.BooleanField(default=False, verbose_name=_('todo')) class Meta: verbose_name = _('Door') verbose_name_plural = _('Doors') default_related_name = 'doors' + def get_geojson_properties(self, *args, **kwargs) -> dict: + result = super().get_geojson_properties(*args, **kwargs) + if self.todo: + result['color'] = "#FFFF00" + return result + + @property + def title(self): + return ('*TODO* ' if self.todo else '') + (self.name or super().title) + class ItemWithValue: def __init__(self, obj, func):