2016-10-13 15:55:15 +02:00
|
|
|
import json
|
2017-07-10 16:00:43 +02:00
|
|
|
import operator
|
2018-11-16 01:26:44 +01:00
|
|
|
import os
|
2017-07-10 16:00:43 +02:00
|
|
|
from functools import reduce
|
2017-12-19 01:49:54 +01:00
|
|
|
from itertools import chain
|
2024-12-19 17:26:50 +01:00
|
|
|
from operator import attrgetter, itemgetter
|
2016-09-23 17:02:17 +02:00
|
|
|
|
2018-11-16 01:26:44 +01:00
|
|
|
from django.conf import settings
|
2017-12-19 01:49:54 +01:00
|
|
|
from django.core.cache import cache
|
2017-07-10 19:03:24 +02:00
|
|
|
from django.core.exceptions import FieldDoesNotExist
|
2018-11-18 02:00:27 +01:00
|
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
2024-02-07 18:34:28 +01:00
|
|
|
from django.db.models import Prefetch, Q
|
2024-12-11 14:43:27 +01:00
|
|
|
from django.db.models.fields.reverse_related import ManyToManyRel
|
2023-12-10 21:59:20 +01:00
|
|
|
from django.forms import (BooleanField, CharField, ChoiceField, DecimalField, Form, JSONField, ModelChoiceField,
|
|
|
|
ModelForm, MultipleChoiceField, Select, ValidationError)
|
2024-01-06 13:26:34 +01:00
|
|
|
from django.forms.widgets import HiddenInput, TextInput
|
2023-12-11 18:48:40 +01:00
|
|
|
from django.utils.translation import get_language
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
2023-12-27 03:51:10 +01:00
|
|
|
from pydantic import ValidationError as PydanticValidationError
|
2024-02-07 18:34:28 +01:00
|
|
|
from shapely.geometry.geo import mapping
|
2016-09-23 15:23:02 +02:00
|
|
|
|
2017-07-05 19:40:35 +02:00
|
|
|
from c3nav.editor.models import ChangeSet, ChangeSetUpdate
|
2017-12-08 00:36:12 +01:00
|
|
|
from c3nav.mapdata.fields import GeometryField
|
|
|
|
from c3nav.mapdata.forms import I18nModelFormMixin
|
2024-08-22 14:08:42 +02:00
|
|
|
from c3nav.mapdata.models import GraphEdge, LocationGroup, Source, LocationGroupCategory, GraphNode, Space, \
|
|
|
|
LocationSlug, WayType
|
|
|
|
from c3nav.mapdata.models.access import AccessPermission, AccessRestrictionGroup, AccessRestriction
|
2024-12-23 16:26:15 +01:00
|
|
|
from c3nav.mapdata.models.geometry.space import ObstacleGroup, BeaconMeasurement
|
2024-08-22 14:08:42 +02:00
|
|
|
from c3nav.mapdata.models.theme import ThemeLocationGroupBackgroundColor, ThemeObstacleGroupBackgroundColor
|
2024-12-23 16:26:15 +01:00
|
|
|
from c3nav.routing.schemas import LocateWifiPeerSchema, BeaconMeasurementDataSchema
|
2017-06-29 15:53:26 +02:00
|
|
|
|
2016-09-23 15:23:02 +02:00
|
|
|
|
2017-12-08 00:36:12 +01:00
|
|
|
class EditorFormBase(I18nModelFormMixin, ModelForm):
|
2018-12-09 21:26:01 +01:00
|
|
|
def __init__(self, *args, space_id=None, request=None, geometry_editable=False, is_json=False, **kwargs):
|
2016-09-26 17:23:08 +02:00
|
|
|
self.request = request
|
2016-09-23 15:23:02 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2016-10-13 15:55:15 +02:00
|
|
|
creating = not self.instance.pk
|
2016-09-23 15:23:02 +02:00
|
|
|
|
2024-01-06 13:26:34 +01:00
|
|
|
if self._meta.model.__name__ == 'Theme':
|
|
|
|
if creating:
|
|
|
|
locationgroup_theme_colors = {}
|
|
|
|
obstaclegroup_theme_colors = {}
|
|
|
|
else:
|
|
|
|
locationgroup_theme_colors = {
|
2024-02-07 18:34:28 +01:00
|
|
|
theme_location_group.location_group_id: theme_location_group
|
|
|
|
for theme_location_group in self.instance.location_groups.filter(theme_id=self.instance.pk)
|
2024-01-06 13:26:34 +01:00
|
|
|
}
|
|
|
|
obstaclegroup_theme_colors = {
|
2024-02-07 18:34:28 +01:00
|
|
|
theme_obstacle.obstacle_group_id: theme_obstacle
|
|
|
|
for theme_obstacle in self.instance.obstacle_groups.filter(theme_id=self.instance.pk)
|
2024-01-06 13:26:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
# TODO: can we get the model class via relationships?
|
|
|
|
for locationgroup in LocationGroup.objects.prefetch_related(
|
|
|
|
Prefetch('theme_colors', ThemeLocationGroupBackgroundColor.objects.only('fill_color'))).all():
|
|
|
|
related = locationgroup_theme_colors.get(locationgroup.pk, None)
|
|
|
|
value = related.fill_color if related is not None else None
|
|
|
|
other_themes_colors = {
|
2024-03-29 13:46:35 +01:00
|
|
|
str(theme_location_group.theme.title): theme_location_group.fill_color
|
2024-02-07 18:34:28 +01:00
|
|
|
for theme_location_group in locationgroup.theme_colors.all()
|
|
|
|
if related is None or theme_location_group.pk != related.pk
|
2024-01-06 13:26:34 +01:00
|
|
|
}
|
|
|
|
if len(other_themes_colors) > 0:
|
|
|
|
other_themes_colors = json.dumps(other_themes_colors)
|
|
|
|
else:
|
|
|
|
other_themes_colors = False
|
2024-02-07 18:34:28 +01:00
|
|
|
field = CharField(
|
|
|
|
max_length=32,
|
|
|
|
label=locationgroup.title,
|
|
|
|
required=False,
|
|
|
|
initial=value,
|
|
|
|
widget=TextInput(attrs={
|
|
|
|
'data-themed-color': True,
|
|
|
|
'data-color-base-theme': locationgroup.color if locationgroup.color else False,
|
|
|
|
'data-colors-other-themes': other_themes_colors,
|
|
|
|
}))
|
2024-01-06 13:26:34 +01:00
|
|
|
self.fields[f'locationgroup_{locationgroup.pk}'] = field
|
|
|
|
|
|
|
|
for obstaclegroup in ObstacleGroup.objects.prefetch_related(
|
|
|
|
Prefetch('theme_colors', ThemeObstacleGroupBackgroundColor.objects.only('fill_color'))).all():
|
|
|
|
related = obstaclegroup_theme_colors.get(obstaclegroup.pk, None)
|
|
|
|
value = related.fill_color if related is not None else None
|
|
|
|
other_themes_colors = {
|
|
|
|
o.title: o.fill_color
|
|
|
|
for o in obstaclegroup.theme_colors.all()
|
|
|
|
if related is None or o.pk != related.pk
|
|
|
|
}
|
|
|
|
if len(other_themes_colors) > 0:
|
|
|
|
other_themes_colors = json.dumps(other_themes_colors)
|
|
|
|
else:
|
|
|
|
other_themes_colors = False
|
|
|
|
field = CharField(max_length=32,
|
|
|
|
label=obstaclegroup.title,
|
|
|
|
required=False,
|
|
|
|
initial=value,
|
|
|
|
widget=TextInput(attrs={
|
|
|
|
'data-themed-color': True,
|
|
|
|
'data-color-base-theme': obstaclegroup.color if obstaclegroup.color else False,
|
|
|
|
'data-colors-other-themes': other_themes_colors,
|
|
|
|
}))
|
|
|
|
self.fields[f'obstaclegroup_{obstaclegroup.pk}'] = field
|
|
|
|
|
2017-12-21 02:35:36 +01:00
|
|
|
if hasattr(self.instance, 'author_id'):
|
|
|
|
if self.instance.author_id is None:
|
|
|
|
self.instance.author = request.user
|
|
|
|
|
2016-11-28 15:46:58 +01:00
|
|
|
if 'geometry' in self.fields:
|
2018-12-09 21:26:01 +01:00
|
|
|
if not geometry_editable:
|
2018-11-20 22:54:29 +01:00
|
|
|
# can't see this geometry in editor
|
|
|
|
self.fields.pop('geometry')
|
|
|
|
else:
|
|
|
|
# hide geometry widget
|
|
|
|
self.fields['geometry'].widget = HiddenInput()
|
|
|
|
if not creating:
|
2023-07-24 11:51:25 +02:00
|
|
|
self.initial['geometry'] = mapping(self.instance.geometry)
|
2016-10-13 15:55:15 +02:00
|
|
|
|
2023-12-19 15:44:00 +01:00
|
|
|
if 'main_point' in self.fields:
|
|
|
|
if not geometry_editable:
|
|
|
|
# can't see this geometry in editor
|
|
|
|
self.fields.pop('main_point')
|
2023-12-20 00:13:36 +01:00
|
|
|
else:
|
|
|
|
# hide geometry widget
|
|
|
|
self.fields['main_point'].widget = HiddenInput()
|
|
|
|
if not creating:
|
2023-12-24 16:37:09 +01:00
|
|
|
self.initial['main_point'] = (
|
|
|
|
mapping(self.instance.main_point)
|
|
|
|
if self.instance.main_point and not self.instance.main_point.is_empty
|
|
|
|
else None
|
|
|
|
)
|
2023-12-19 15:44:00 +01:00
|
|
|
|
2018-11-16 01:26:44 +01:00
|
|
|
if self._meta.model.__name__ == 'Source' and self.request.user.is_superuser:
|
2018-11-18 02:00:27 +01:00
|
|
|
sources = {s['name']: s for s in Source.objects.all().values('name', 'access_restriction_id',
|
|
|
|
'left', 'bottom', 'right', 'top')}
|
|
|
|
used_names = set(sources.keys())
|
2018-12-13 04:05:23 +01:00
|
|
|
all_names = set(os.listdir(settings.SOURCES_ROOT))
|
2018-12-13 03:57:03 +01:00
|
|
|
if not creating:
|
|
|
|
used_names.remove(self.instance.name)
|
2018-12-13 04:05:23 +01:00
|
|
|
all_names.add(self.instance.name)
|
2018-11-16 01:26:44 +01:00
|
|
|
self.fields['name'].widget = Select(choices=tuple((s, s) for s in sorted(all_names-used_names)))
|
|
|
|
|
2018-11-18 02:00:27 +01:00
|
|
|
if creating:
|
|
|
|
for s in sources.values():
|
|
|
|
s['access_restriction'] = s['access_restriction_id']
|
|
|
|
del s['access_restriction_id']
|
|
|
|
self.fields['copy_from'] = ChoiceField(
|
|
|
|
choices=tuple((('', '---------'), ))+tuple(
|
|
|
|
(json.dumps(sources[name], separators=(',', ':'), cls=DjangoJSONEncoder), name)
|
|
|
|
for name in sorted(used_names)
|
|
|
|
),
|
|
|
|
required=False
|
|
|
|
)
|
|
|
|
|
2018-11-17 01:40:57 +01:00
|
|
|
self.fields['fixed_x'] = DecimalField(label='fixed x', required=False,
|
|
|
|
max_digits=7, decimal_places=3, initial=0)
|
|
|
|
self.fields['fixed_y'] = DecimalField(label='fixed y', required=False,
|
|
|
|
max_digits=7, decimal_places=3, initial=0)
|
|
|
|
self.fields['scale_x'] = DecimalField(label='scale x (m/px)', required=False,
|
2024-09-04 12:49:27 +02:00
|
|
|
max_digits=7, decimal_places=6, initial=1)
|
2018-11-17 01:40:57 +01:00
|
|
|
self.fields['scale_y'] = DecimalField(label='scale y (m/px)', required=False,
|
2024-09-04 12:49:27 +02:00
|
|
|
max_digits=7, decimal_places=6, initial=1)
|
2018-11-17 01:40:57 +01:00
|
|
|
self.fields['lock_aspect'] = BooleanField(label='lock aspect ratio', required=False, initial=True)
|
|
|
|
self.fields['lock_scale'] = BooleanField(label='lock scale (for moving)', required=False, initial=True)
|
|
|
|
|
|
|
|
self.fields.move_to_end('lock_scale', last=False)
|
|
|
|
self.fields.move_to_end('lock_aspect', last=False)
|
|
|
|
self.fields.move_to_end('scale_y', last=False)
|
|
|
|
self.fields.move_to_end('scale_x', last=False)
|
|
|
|
self.fields.move_to_end('fixed_y', last=False)
|
|
|
|
self.fields.move_to_end('fixed_x', last=False)
|
|
|
|
self.fields.move_to_end('access_restriction', last=False)
|
2018-11-18 02:00:27 +01:00
|
|
|
if creating:
|
|
|
|
self.fields.move_to_end('copy_from', last=False)
|
2018-11-17 01:40:57 +01:00
|
|
|
self.fields.move_to_end('name', last=False)
|
|
|
|
|
2024-12-11 14:43:27 +01:00
|
|
|
if self._meta.model.__name__ == 'AccessRestrictionGroup':
|
|
|
|
self.fields['members'].label_from_instance = lambda obj: obj.title
|
|
|
|
self.fields['members'].queryset = AccessRestriction.qs_for_request(self.request)
|
2017-12-20 20:54:58 +01:00
|
|
|
|
|
|
|
elif 'groups' in self.fields:
|
2017-07-10 19:04:35 +02:00
|
|
|
kwargs = {'allow_'+self._meta.model._meta.default_related_name: True}
|
2018-12-21 19:07:28 +01:00
|
|
|
categories = LocationGroupCategory.objects.filter(**kwargs).prefetch_related('groups')
|
2017-07-10 19:18:44 +02:00
|
|
|
if self.instance.pk:
|
|
|
|
instance_groups = tuple(self.instance.groups.values_list('pk', flat=True))
|
|
|
|
else:
|
|
|
|
instance_groups = ()
|
2017-07-10 16:00:43 +02:00
|
|
|
|
|
|
|
self.fields.pop('groups')
|
|
|
|
|
|
|
|
for category in categories:
|
2018-12-21 20:00:52 +01:00
|
|
|
choices = tuple((str(group.pk), group.title)
|
|
|
|
for group in sorted(category.groups.all(), key=self.sort_group))
|
2017-07-10 19:18:44 +02:00
|
|
|
category_groups = set(group.pk for group in category.groups.all())
|
|
|
|
initial = tuple(str(pk) for pk in instance_groups if pk in category_groups)
|
|
|
|
if category.single:
|
|
|
|
name = 'group_'+category.name
|
|
|
|
initial = initial[0] if initial else ''
|
2017-07-11 18:04:22 +02:00
|
|
|
choices = (('', '---'), )+choices
|
2018-12-23 17:52:15 +01:00
|
|
|
field = ChoiceField(label=category.title, required=False, initial=initial, choices=choices,
|
|
|
|
help_text=category.help_text)
|
2017-07-10 19:18:44 +02:00
|
|
|
else:
|
|
|
|
name = 'groups_'+category.name
|
2017-11-30 16:51:28 +01:00
|
|
|
field = MultipleChoiceField(label=category.title_plural, required=False,
|
2018-12-23 17:52:15 +01:00
|
|
|
initial=initial, choices=choices,
|
|
|
|
help_text=category.help_text)
|
2017-07-10 19:18:44 +02:00
|
|
|
self.fields[name] = field
|
2017-05-16 14:50:36 +02:00
|
|
|
|
2019-12-22 00:38:54 +01:00
|
|
|
if 'label_settings' in self.fields:
|
|
|
|
self.fields.move_to_end('label_settings')
|
|
|
|
|
|
|
|
for field in tuple(self.fields.keys()):
|
|
|
|
if field.startswith('label_override'):
|
|
|
|
self.fields.move_to_end(field)
|
|
|
|
|
2023-12-11 18:17:39 +01:00
|
|
|
if 'groundaltitude' in self.fields:
|
2023-12-11 22:11:08 +01:00
|
|
|
self.fields['groundaltitude'].label_from_instance = attrgetter('choice_label')
|
2023-12-11 18:17:39 +01:00
|
|
|
|
2024-12-26 02:56:20 +01:00
|
|
|
for name in ('category', 'label_settings', 'load_group_contribute', 'load_group_display'):
|
|
|
|
if name in self.fields:
|
|
|
|
self.fields[name].label_from_instance = attrgetter('title')
|
2017-07-10 14:10:48 +02:00
|
|
|
|
2017-07-13 18:54:49 +02:00
|
|
|
if 'access_restriction' in self.fields:
|
|
|
|
self.fields['access_restriction'].label_from_instance = lambda obj: obj.title
|
2023-12-08 20:55:36 +01:00
|
|
|
self.fields['access_restriction'].queryset = AccessRestriction.qs_for_request(self.request).order_by(
|
|
|
|
"titles__"+get_language(), "titles__en"
|
|
|
|
)
|
2017-07-13 18:54:49 +02:00
|
|
|
|
2018-11-20 23:06:49 +01:00
|
|
|
if 'base_mapdata_accessible' in self.fields:
|
|
|
|
if not request.user.is_superuser:
|
|
|
|
self.fields['base_mapdata_accessible'].disabled = True
|
|
|
|
|
2017-12-19 01:49:54 +01:00
|
|
|
if space_id and 'target_space' in self.fields:
|
|
|
|
cache_key = 'editor:neighbor_spaces:%s:%s%d' % (
|
|
|
|
self.request.changeset.raw_cache_key_by_changes,
|
|
|
|
AccessPermission.cache_key_for_request(request, with_update=False),
|
|
|
|
space_id
|
|
|
|
)
|
|
|
|
other_spaces = cache.get(cache_key, None)
|
|
|
|
if other_spaces is None:
|
|
|
|
AccessPermission.cache_key_for_request(request, with_update=False) + ':' + str(request.user.pk or 0)
|
|
|
|
space_nodes = set(GraphNode.objects.filter(space_id=space_id).values_list('pk', flat=True))
|
|
|
|
space_edges = GraphEdge.objects.filter(
|
|
|
|
Q(from_node_id__in=space_nodes) | Q(to_node_id__in=space_nodes)
|
|
|
|
).values_list('from_node_id', 'to_node_id')
|
|
|
|
other_nodes = set(chain(*space_edges)) - space_nodes
|
|
|
|
other_spaces = set(GraphNode.objects.filter(pk__in=other_nodes).values_list('space_id', flat=True))
|
|
|
|
other_spaces.discard(space_id)
|
|
|
|
cache.set(cache_key, other_spaces, 900)
|
|
|
|
|
|
|
|
for space_field in ('origin_space', 'target_space'):
|
|
|
|
other_space_id = getattr(self.instance, space_field+'_id', None)
|
|
|
|
if other_space_id:
|
|
|
|
other_spaces.add(other_space_id)
|
|
|
|
|
|
|
|
space_qs = Space.qs_for_request(self.request).filter(pk__in=other_spaces)
|
|
|
|
|
2017-12-19 00:56:12 +01:00
|
|
|
for space_field in ('origin_space', 'target_space'):
|
|
|
|
if space_field in self.fields:
|
|
|
|
self.fields[space_field].label_from_instance = lambda obj: obj.title
|
|
|
|
self.fields[space_field].queryset = space_qs
|
2017-12-19 00:50:40 +01:00
|
|
|
|
2017-05-27 18:29:36 +02:00
|
|
|
self.redirect_slugs = None
|
|
|
|
self.add_redirect_slugs = None
|
|
|
|
self.remove_redirect_slugs = None
|
|
|
|
if 'slug' in self.fields:
|
2023-06-26 17:32:37 +02:00
|
|
|
self.redirect_slugs = (sorted(self.instance.redirects.values_list('slug', flat=True))
|
|
|
|
if self.instance.pk else [])
|
2020-01-27 01:53:00 +01:00
|
|
|
self.fields['redirect_slugs'] = CharField(label=_('Redirecting Slugs (comma separated)'), required=False,
|
2017-05-27 18:29:36 +02:00
|
|
|
initial=','.join(self.redirect_slugs))
|
|
|
|
self.fields.move_to_end('redirect_slugs', last=False)
|
|
|
|
self.fields.move_to_end('slug', last=False)
|
|
|
|
|
2017-07-19 18:02:40 +02:00
|
|
|
if 'from_node' in self.fields:
|
|
|
|
self.fields['from_node'].widget = HiddenInput()
|
|
|
|
|
|
|
|
if 'to_node' in self.fields:
|
|
|
|
self.fields['to_node'].widget = HiddenInput()
|
|
|
|
|
2018-11-22 19:14:36 +01:00
|
|
|
self.is_json = is_json
|
|
|
|
self.missing_fields = tuple((name, field) for name, field in self.fields.items()
|
|
|
|
if name not in self.data and not field.required)
|
|
|
|
|
2018-12-21 20:00:52 +01:00
|
|
|
@staticmethod
|
|
|
|
def sort_group(group):
|
|
|
|
return (-group.priority, group.title)
|
|
|
|
|
2017-05-27 18:29:36 +02:00
|
|
|
def clean_redirect_slugs(self):
|
|
|
|
old_redirect_slugs = set(self.redirect_slugs)
|
|
|
|
new_redirect_slugs = set(s for s in (s.strip() for s in self.cleaned_data['redirect_slugs'].split(',')) if s)
|
|
|
|
|
|
|
|
self.add_redirect_slugs = new_redirect_slugs - old_redirect_slugs
|
|
|
|
self.remove_redirect_slugs = old_redirect_slugs - new_redirect_slugs
|
|
|
|
|
2018-12-25 23:28:22 +01:00
|
|
|
model_slug_field = self._meta.model._meta.get_field('slug')
|
2017-05-27 18:29:36 +02:00
|
|
|
for slug in self.add_redirect_slugs:
|
|
|
|
self.fields['slug'].run_validators(slug)
|
2018-12-25 23:28:22 +01:00
|
|
|
model_slug_field.run_validators(slug)
|
2017-06-12 18:01:51 +02:00
|
|
|
|
2017-07-06 12:26:19 +02:00
|
|
|
qs = LocationSlug.objects.filter(slug__in=self.add_redirect_slugs)
|
|
|
|
|
2017-08-04 20:17:08 +02:00
|
|
|
if 'slug' in self.cleaned_data and self.cleaned_data['slug'] in self.add_redirect_slugs:
|
2017-07-06 12:26:19 +02:00
|
|
|
raise ValidationError(
|
|
|
|
_('Can not add redirecting slug “%s”: it\'s the slug of this object.') % self.cleaned_data['slug']
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
qs = qs.exclude(pk=self.instance.pk)
|
|
|
|
|
|
|
|
for slug in qs.values_list('slug', flat=True)[:1]:
|
2017-06-12 18:01:51 +02:00
|
|
|
raise ValidationError(
|
|
|
|
_('Can not add redirecting slug “%s”: it is already used elsewhere.') % slug
|
|
|
|
)
|
2017-05-27 18:29:36 +02:00
|
|
|
|
2017-12-21 03:32:03 +01:00
|
|
|
def clean_data(self):
|
2024-12-23 16:26:15 +01:00
|
|
|
data = self.cleaned_data['data']
|
|
|
|
if not data.wifi:
|
2024-08-30 20:47:35 +02:00
|
|
|
raise ValidationError(_('WiFi scan data is missing.'))
|
2024-12-23 16:26:15 +01:00
|
|
|
data.wifi = [[item for item in scan if item.ssid] for scan in data.wifi]
|
2017-12-21 03:32:03 +01:00
|
|
|
return data
|
|
|
|
|
2016-11-28 15:46:58 +01:00
|
|
|
def clean(self):
|
2018-11-22 19:14:36 +01:00
|
|
|
if self.is_json:
|
|
|
|
for name, field in self.missing_fields:
|
|
|
|
self.add_error(name, field.error_messages['required'])
|
|
|
|
|
2016-11-28 15:46:58 +01:00
|
|
|
if 'geometry' in self.fields:
|
|
|
|
if not self.cleaned_data.get('geometry'):
|
|
|
|
raise ValidationError('Missing geometry.')
|
2016-12-12 13:21:09 +01:00
|
|
|
|
2016-12-04 14:16:05 +01:00
|
|
|
super().clean()
|
2016-11-28 15:46:58 +01:00
|
|
|
|
2017-07-10 16:00:43 +02:00
|
|
|
def _save_m2m(self):
|
|
|
|
super()._save_m2m()
|
2017-12-20 22:44:45 +01:00
|
|
|
if self._meta.model.__name__ != 'AccessRestriction':
|
|
|
|
try:
|
|
|
|
field = self._meta.model._meta.get_field('groups')
|
|
|
|
except FieldDoesNotExist:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
if field.many_to_many:
|
|
|
|
groups = reduce(operator.or_, (set(value) for name, value in self.cleaned_data.items()
|
|
|
|
if name.startswith('groups_')), set())
|
|
|
|
groups |= set(value for name, value in self.cleaned_data.items()
|
|
|
|
if name.startswith('group_') and value)
|
|
|
|
groups = tuple((int(val) if val.isdigit() else val) for val in groups)
|
|
|
|
self.instance.groups.set(groups)
|
2017-07-10 16:00:43 +02:00
|
|
|
|
2024-01-06 13:26:34 +01:00
|
|
|
if self._meta.model.__name__ == 'Theme':
|
2024-02-07 18:34:28 +01:00
|
|
|
locationgroup_colors = {theme_location_group.location_group_id: theme_location_group
|
|
|
|
for theme_location_group in self.instance.location_groups.all()}
|
2024-01-06 13:26:34 +01:00
|
|
|
for locationgroup in LocationGroup.objects.all():
|
|
|
|
value = self.cleaned_data[f'locationgroup_{locationgroup.pk}']
|
|
|
|
if value:
|
|
|
|
color = locationgroup_colors.get(locationgroup.pk,
|
|
|
|
ThemeLocationGroupBackgroundColor(theme=self.instance,
|
|
|
|
location_group=locationgroup))
|
|
|
|
color.fill_color = value
|
|
|
|
color.save()
|
|
|
|
else:
|
|
|
|
color = locationgroup_colors.get(locationgroup.pk, None)
|
|
|
|
if color is not None:
|
|
|
|
color.delete()
|
|
|
|
|
|
|
|
obstaclegroup_colors = {o.obstacle_group_id: o for o in self.instance.obstacle_groups.all()}
|
|
|
|
for obstaclegroup in ObstacleGroup.objects.all():
|
|
|
|
value = self.cleaned_data[f'obstaclegroup_{obstaclegroup.pk}']
|
|
|
|
if value:
|
|
|
|
color = obstaclegroup_colors.get(obstaclegroup.pk,
|
|
|
|
ThemeObstacleGroupBackgroundColor(theme=self.instance,
|
|
|
|
obstacle_group=obstaclegroup))
|
|
|
|
color.fill_color = value
|
|
|
|
color.save()
|
|
|
|
else:
|
|
|
|
color = obstaclegroup_colors.get(obstaclegroup.pk)
|
|
|
|
if color is not None:
|
|
|
|
color.delete()
|
|
|
|
|
2016-11-28 15:46:58 +01:00
|
|
|
|
2017-05-16 14:10:50 +02:00
|
|
|
def create_editor_form(editor_model):
|
2024-12-16 15:54:45 +01:00
|
|
|
possible_fields = [
|
2024-12-19 17:26:50 +01:00
|
|
|
'slug', 'name', 'title', 'title_plural', 'help_text', 'position_secret', 'icon', 'join_edges', 'todo',
|
2024-12-19 11:59:11 +01:00
|
|
|
'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',
|
2024-12-19 10:56:38 +01:00
|
|
|
'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside',
|
|
|
|
'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'level_index', 'short_label',
|
|
|
|
'origin_space', 'target_space', 'data',
|
2024-12-16 15:54:45 +01:00
|
|
|
'comment', 'slow_down_factor', 'groundaltitude', 'node_number', 'wifi_bssid', 'bluetooth_address', "group",
|
|
|
|
'ibeacon_uuid', 'ibeacon_major', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing',
|
|
|
|
"can_report_mistake", 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text',
|
|
|
|
'enter_description', 'level_change_description', 'base_mapdata_accessible', 'label_settings', 'label_override',
|
|
|
|
'min_zoom', 'max_zoom', 'font_size', 'members', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois',
|
|
|
|
'allow_dynamic_locations', 'left', 'top', 'right', 'bottom', 'import_tag', 'import_block_data',
|
|
|
|
'import_block_geom', 'public', 'default', 'dark', 'high_contrast', 'funky', 'randomize_primary_color',
|
|
|
|
'color_logo', 'color_css_initial', 'color_css_primary', 'color_css_secondary', 'color_css_tertiary',
|
|
|
|
'color_css_quaternary', 'color_css_quinary', 'color_css_header_background', 'color_css_header_text',
|
|
|
|
'color_css_header_text_hover', 'color_css_shadow', 'color_css_overlay_background', 'color_css_grid',
|
|
|
|
'color_css_modal_backdrop', 'color_css_route_dots_shadow', 'extra_css', 'icon_path', 'leaflet_marker_config',
|
|
|
|
'color_background', 'color_wall_fill', 'color_wall_border', 'color_door_fill', 'color_ground_fill',
|
|
|
|
'color_obstacles_default_fill', 'color_obstacles_default_border', 'stroke_color', 'stroke_width',
|
|
|
|
'stroke_opacity', 'fill_color', 'fill_opacity', 'interactive', 'point_icon', 'extra_data', 'show_label',
|
2024-12-19 11:59:11 +01:00
|
|
|
'show_geometry', 'show_label', 'show_geometry', 'default_geomtype',
|
2024-12-25 17:01:30 +01:00
|
|
|
"load_group_display", "load_group_contribute",
|
2024-12-24 20:20:01 +01:00
|
|
|
"altitude_quest",
|
2024-12-16 15:54:45 +01:00
|
|
|
]
|
2024-12-11 14:43:27 +01:00
|
|
|
field_names = [field.name for field in editor_model._meta.get_fields()
|
|
|
|
if not field.one_to_many and not isinstance(field, ManyToManyRel)]
|
2017-05-26 20:40:20 +02:00
|
|
|
existing_fields = [name for name in possible_fields if name in field_names]
|
2016-10-13 15:55:15 +02:00
|
|
|
|
2017-07-10 19:03:45 +02:00
|
|
|
class EditorForm(EditorFormBase, ModelForm):
|
2016-10-13 15:55:15 +02:00
|
|
|
class Meta:
|
2017-05-16 14:10:50 +02:00
|
|
|
model = editor_model
|
2016-11-28 15:46:58 +01:00
|
|
|
fields = existing_fields
|
2016-10-13 15:55:15 +02:00
|
|
|
|
2017-06-21 12:47:28 +02:00
|
|
|
EditorForm.__name__ = editor_model.__name__+'EditorForm'
|
|
|
|
return EditorForm
|
2017-06-29 15:53:26 +02:00
|
|
|
|
|
|
|
|
2024-08-22 14:08:42 +02:00
|
|
|
editor_form_cache = {}
|
2024-12-26 02:20:25 +01:00
|
|
|
|
|
|
|
|
2024-08-22 14:08:42 +02:00
|
|
|
def get_editor_form(model):
|
|
|
|
form = editor_form_cache.get(model, None)
|
|
|
|
if form is None:
|
|
|
|
form = create_editor_form(model)
|
|
|
|
editor_form_cache[model] = form
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2017-06-29 15:53:26 +02:00
|
|
|
class ChangeSetForm(ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = ChangeSet
|
|
|
|
fields = ('title', 'description')
|
2017-07-05 19:40:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
class RejectForm(ModelForm):
|
|
|
|
final = BooleanField(label=_('Final rejection'), required=False)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = ChangeSetUpdate
|
|
|
|
fields = ('comment', )
|
2017-07-19 18:02:40 +02:00
|
|
|
|
|
|
|
|
2017-07-26 13:20:55 +02:00
|
|
|
class GraphEdgeSettingsForm(ModelForm):
|
2017-11-22 23:13:12 +01:00
|
|
|
oneway = BooleanField(label=_('create one way edges'), required=False)
|
2017-11-25 16:25:50 +01:00
|
|
|
activate_next = BooleanField(label=_('activate next node after connecting'), required=False)
|
2017-11-22 23:13:12 +01:00
|
|
|
|
2017-07-26 13:20:55 +02:00
|
|
|
class Meta:
|
|
|
|
model = GraphEdge
|
|
|
|
fields = ('waytype', 'access_restriction', )
|
|
|
|
|
|
|
|
def __init__(self, *args, request=None, **kwargs):
|
|
|
|
self.request = request
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
2017-07-27 18:58:40 +02:00
|
|
|
self.fields['waytype'].label_from_instance = lambda obj: obj.title
|
|
|
|
self.fields['waytype'].queryset = WayType.objects.all()
|
|
|
|
self.fields['waytype'].to_field_name = None
|
|
|
|
|
2017-07-26 13:20:55 +02:00
|
|
|
self.fields['access_restriction'].label_from_instance = lambda obj: obj.title
|
|
|
|
self.fields['access_restriction'].queryset = AccessRestriction.qs_for_request(self.request)
|
|
|
|
|
|
|
|
|
|
|
|
class GraphEditorActionForm(Form):
|
|
|
|
def __init__(self, *args, request=None, allow_clicked_position=False, **kwargs):
|
|
|
|
self.request = request
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
graph_node_qs = GraphNode.objects.all()
|
2017-07-26 13:30:08 +02:00
|
|
|
self.fields['active_node'] = ModelChoiceField(graph_node_qs, widget=HiddenInput(), required=False)
|
2017-07-26 13:20:55 +02:00
|
|
|
self.fields['clicked_node'] = ModelChoiceField(graph_node_qs, widget=HiddenInput(), required=False)
|
|
|
|
|
|
|
|
if allow_clicked_position:
|
2023-12-10 21:59:20 +01:00
|
|
|
self.fields['clicked_position'] = JSONField(widget=HiddenInput(), required=False)
|
2017-07-26 13:20:55 +02:00
|
|
|
|
2017-07-26 15:28:08 +02:00
|
|
|
space_qs = Space.objects.all()
|
|
|
|
self.fields['goto_space'] = ModelChoiceField(space_qs, widget=HiddenInput(), required=False)
|
|
|
|
|
2017-07-26 13:20:55 +02:00
|
|
|
def clean_clicked_position(self):
|
|
|
|
return GeometryField(geomtype='point').to_python(self.cleaned_data['clicked_position'])
|
2024-12-19 17:26:50 +01:00
|
|
|
|
|
|
|
|
|
|
|
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()
|