doors now have todos, names and a cooler new UI

This commit is contained in:
Laura Klünder 2024-12-19 17:26:50 +01:00
parent e42c0e8111
commit 870c26e335
5 changed files with 132 additions and 7 deletions

View file

@ -3,7 +3,7 @@ import operator
import os import os
from functools import reduce from functools import reduce
from itertools import chain from itertools import chain
from operator import attrgetter from operator import attrgetter, itemgetter
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -398,7 +398,7 @@ class EditorFormBase(I18nModelFormMixin, ModelForm):
def create_editor_form(editor_model): def create_editor_form(editor_model):
possible_fields = [ 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', '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', 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name',
'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside', 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside',
@ -493,3 +493,39 @@ class GraphEditorActionForm(Form):
def clean_clicked_position(self): def clean_clicked_position(self):
return GeometryField(geomtype='point').to_python(self.cleaned_data['clicked_position']) 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()

View file

@ -23,6 +23,24 @@
<form action="{{ path }}" method="post" {% if nozoom %}data-nozoom {% endif %}data-onbeforeunload {% if new %}data-new="{{ model_name }}" data-geomtype="{{ geomtype }}" {% if default_geomtype %}data-default-geomtype="{{ default_geomtype }}{% endif %}"{% else %}data-editing="{{ model_name }}-{{ pk }}"{% endif %}{% if access_restriction_select %} data-access-restriction-select{% endif %}> <form action="{{ path }}" method="post" {% if nozoom %}data-nozoom {% endif %}data-onbeforeunload {% if new %}data-new="{{ model_name }}" data-geomtype="{{ geomtype }}" {% if default_geomtype %}data-default-geomtype="{{ default_geomtype }}{% endif %}"{% else %}data-editing="{{ model_name }}-{{ pk }}"{% endif %}{% if access_restriction_select %} data-access-restriction-select{% endif %}>
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% if door %}
<hr>
<h4>{% trans 'Connecting spaces:' %}</h4>
<ul>
{% for space in door.spaces.values %}
<li>
{{ space.title }}
{% if space.access_restriction %}
<small>({{ space.access_restriction.title }})</small>
{% else %}
<small>(<em>no restriction</em>)</small>
{% endif %}
</li>
{% endfor %}
</ul>
{% bootstrap_form door.form %}
<hr>
{% endif %}
{% buttons %} {% buttons %}
<button class="invisiblesubmit" type="submit"></button> <button class="invisiblesubmit" type="submit"></button>
{% if can_edit %} {% if can_edit %}

View file

@ -6,7 +6,7 @@ from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.cache import cache 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 import IntegrityError, models
from django.db.models import Q from django.db.models import Q
from django.http import Http404, HttpResponse 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.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import etag 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.utils import DefaultEditUtils, LevelChildEditUtils, SpaceChildEditUtils
from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse, from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse,
APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse, APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse,
editor_etag_func, sidebar_view, accesses_mapdata) 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.models.access import AccessPermission, AccessRestriction, AccessRestrictionGroup
from c3nav.mapdata.utils.geometry import unwrap_geom
from c3nav.mapdata.utils.user import can_access_editor 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', return APIHybridLoginRequiredResponse(next=request.path_info, login_url='editor.login', level='info',
message=_('You need to log in to create Beacon Measurements.')) 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 error = None
delete = getattr(request, 'is_delete', None) delete = getattr(request, 'is_delete', None)
if request.method == 'POST' or (not new and delete): 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, 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, request=request, space_id=space_id,
geometry_editable=edit_utils.can_access_child_base_mapdata) 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 # Update/create objects
obj = form.save(commit=False) 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: for slug in form.remove_redirect_slugs:
obj.redirects.filter(slug=slug).delete() obj.redirects.filter(slug=slug).delete()
if graph_form is not None:
graph_form.save()
form.save_m2m() form.save_m2m()
return APIHybridMessageRedirectResponse( return APIHybridMessageRedirectResponse(
level='success', level='success',
@ -371,6 +404,9 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
else: else:
form = get_editor_form(model)(instance=obj, request=request, space_id=space_id, form = get_editor_form(model)(instance=obj, request=request, space_id=space_id,
geometry_editable=edit_utils.can_access_child_base_mapdata) 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({ ctx.update({
'form': form, 'form': form,

View file

@ -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'),
),
]

View file

@ -151,12 +151,24 @@ class Door(LevelGeometryMixin, AccessRestrictionMixin, models.Model):
A connection between two spaces A connection between two spaces
""" """
geometry = GeometryField('polygon') 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: class Meta:
verbose_name = _('Door') verbose_name = _('Door')
verbose_name_plural = _('Doors') verbose_name_plural = _('Doors')
default_related_name = '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: class ItemWithValue:
def __init__(self, obj, func): def __init__(self, obj, func):