doors now have todos, names and a cooler new UI
This commit is contained in:
parent
e42c0e8111
commit
870c26e335
5 changed files with 132 additions and 7 deletions
|
@ -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()
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
23
src/c3nav/mapdata/migrations/0123_door_name_door_todo.py
Normal file
23
src/c3nav/mapdata/migrations/0123_door_name_door_todo.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue