rudimentary theme support
This commit is contained in:
parent
b44197a1c5
commit
9399e5c377
23 changed files with 925 additions and 307 deletions
|
@ -305,8 +305,8 @@ def get_space_geometries_result(request, space_id: int, update_cache_key: str, u
|
|||
space.holes.all().only('geometry', 'space'),
|
||||
space.stairs.all().only('geometry', 'space'),
|
||||
space.ramps.all().only('geometry', 'space'),
|
||||
space.obstacles.all().only('geometry', 'space', 'color'),
|
||||
space.lineobstacles.all().only('geometry', 'width', 'space', 'color'),
|
||||
space.obstacles.all().only('geometry', 'space').prefetch_related('group'),
|
||||
space.lineobstacles.all().only('geometry', 'width', 'space').prefetch_related('group'),
|
||||
space.columns.all().only('geometry', 'space'),
|
||||
space.altitudemarkers.all().only('geometry', 'space'),
|
||||
space.wifi_measurements.all().only('geometry', 'space'),
|
||||
|
|
|
@ -9,10 +9,10 @@ from django.conf import settings
|
|||
from django.core.cache import cache
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, Prefetch
|
||||
from django.forms import (BooleanField, CharField, ChoiceField, DecimalField, Form, JSONField, ModelChoiceField,
|
||||
ModelForm, MultipleChoiceField, Select, ValidationError)
|
||||
from django.forms.widgets import HiddenInput
|
||||
from django.forms.widgets import HiddenInput, TextInput
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from shapely.geometry.geo import mapping
|
||||
|
@ -21,8 +21,10 @@ from pydantic import ValidationError as PydanticValidationError
|
|||
from c3nav.editor.models import ChangeSet, ChangeSetUpdate
|
||||
from c3nav.mapdata.fields import GeometryField
|
||||
from c3nav.mapdata.forms import I18nModelFormMixin
|
||||
from c3nav.mapdata.models import GraphEdge
|
||||
from c3nav.mapdata.models import GraphEdge, LocationGroup
|
||||
from c3nav.mapdata.models.access import AccessPermission
|
||||
from c3nav.mapdata.models.geometry.space import ObstacleGroup
|
||||
from c3nav.mapdata.models.theme import ThemeLocationGroupBackgroundColor, ThemeObstacleGroupBackgroundColor
|
||||
from c3nav.routing.schemas import LocateRequestPeerSchema
|
||||
|
||||
|
||||
|
@ -32,6 +34,69 @@ class EditorFormBase(I18nModelFormMixin, ModelForm):
|
|||
super().__init__(*args, **kwargs)
|
||||
creating = not self.instance.pk
|
||||
|
||||
if self._meta.model.__name__ == 'Theme':
|
||||
if creating:
|
||||
locationgroup_theme_colors = {}
|
||||
obstaclegroup_theme_colors = {}
|
||||
else:
|
||||
locationgroup_theme_colors = {
|
||||
l.location_group_id: l
|
||||
for l in self.instance.location_groups.filter(theme_id=self.instance.pk)
|
||||
}
|
||||
obstaclegroup_theme_colors = {
|
||||
o.obstacle_group_id: o
|
||||
for o in self.instance.obstacle_groups.filter(theme_id=self.instance.pk)
|
||||
}
|
||||
|
||||
# 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 = {
|
||||
l.title: l.fill_color
|
||||
for l in locationgroup.theme_colors.all()
|
||||
if related is None or l.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=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,
|
||||
}))
|
||||
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
|
||||
|
||||
if hasattr(self.instance, 'author_id'):
|
||||
if self.instance.author_id is None:
|
||||
self.instance.author = request.user
|
||||
|
@ -309,6 +374,35 @@ class EditorFormBase(I18nModelFormMixin, ModelForm):
|
|||
groups = tuple((int(val) if val.isdigit() else val) for val in groups)
|
||||
self.instance.groups.set(groups)
|
||||
|
||||
if self._meta.model.__name__ == 'Theme':
|
||||
locationgroup_colors = {l.location_group_id: l for l in self.instance.location_groups.all()}
|
||||
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()
|
||||
|
||||
|
||||
def create_editor_form(editor_model):
|
||||
possible_fields = ['slug', 'name', 'title', 'title_plural', 'help_text', 'position_secret',
|
||||
|
|
|
@ -459,3 +459,31 @@ body:not(.mobileclient) .wificollector .btn {
|
|||
.wificollector table tr td {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
|
||||
.theme-editor-filter {
|
||||
body > & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
border-top: 1px solid #e7e7e7;
|
||||
padding-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
body > .theme-color-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label.theme-color-label {
|
||||
display: block;
|
||||
> .theme-color-info {
|
||||
float: right;
|
||||
text-decoration: underline black dotted;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-color-hidden {
|
||||
display: none;
|
||||
}
|
|
@ -72,6 +72,7 @@ editor = {
|
|||
|
||||
editor.init_geometries();
|
||||
editor.init_wificollector();
|
||||
editor.sidebar_content_loaded();
|
||||
},
|
||||
_inform_mobile_client: function(elem) {
|
||||
if (!window.mobileclient || !elem.length) return;
|
||||
|
@ -199,6 +200,67 @@ editor = {
|
|||
}
|
||||
level_control.current_id = parseInt(level_list.attr('data-current-id'));
|
||||
},
|
||||
|
||||
sidebar_content_loaded: function() {
|
||||
if (document.querySelector('#sidebar [data-themed-color]')) {
|
||||
editor.theme_editor_loaded();
|
||||
}
|
||||
},
|
||||
theme_editor_loaded: function() {
|
||||
const filter_show_all = () => {
|
||||
for (const input of document.querySelectorAll('#sidebar [data-themed-color]')) {
|
||||
input.parentElement.classList.remove('theme-color-hidden');
|
||||
}
|
||||
};
|
||||
const filter_show_base = () => {
|
||||
for (const input of document.querySelectorAll('#sidebar [data-themed-color]')) {
|
||||
input.parentElement.classList.toggle('theme-color-hidden',
|
||||
!('colorBaseTheme' in input.dataset));
|
||||
}
|
||||
};
|
||||
const filter_show_any = () => {
|
||||
for (const input of document.querySelectorAll('#sidebar [data-themed-color]')) {
|
||||
input.parentElement.classList.toggle('theme-color-hidden',
|
||||
!('colorBaseTheme' in input.dataset || 'colorsOtherThemes' in input.dataset));
|
||||
}
|
||||
};
|
||||
|
||||
const filterButtons = document.querySelector('body>.theme-editor-filter').cloneNode(true);
|
||||
const first_color_input = document.querySelector('#sidebar [data-themed-color]:first-of-type');
|
||||
first_color_input.parentElement.before(filterButtons);
|
||||
filterButtons.addEventListener('click', e => {
|
||||
const btn = e.target;
|
||||
if (btn.classList.contains('active')) return;
|
||||
for (const b of filterButtons.querySelectorAll('button')) {
|
||||
b.classList.remove('active');
|
||||
}
|
||||
btn.classList.add('active');
|
||||
if ('all' in btn.dataset) filter_show_all();
|
||||
else if ('baseTheme' in btn.dataset) filter_show_base();
|
||||
else if ('anyTheme' in btn.dataset) filter_show_any();
|
||||
});
|
||||
|
||||
const baseInfoElement = document.querySelector('body>.theme-color-info');
|
||||
|
||||
for (const color_input of document.querySelectorAll('#sidebar [data-themed-color]')) {
|
||||
let colors = {};
|
||||
if ('colorBaseTheme' in color_input.dataset) {
|
||||
colors.base = color_input.dataset.colorBaseTheme;
|
||||
}
|
||||
if ('colorsOtherThemes' in color_input.dataset) {
|
||||
const other_themes = JSON.parse(color_input.dataset.colorsOtherThemes);
|
||||
colors = {...colors, ...other_themes};
|
||||
}
|
||||
const titleStr = Object.entries(colors).map(([theme, color]) => `${theme}: ${color}`).join(' ');
|
||||
if (!titleStr) continue;
|
||||
const infoElement = baseInfoElement.cloneNode(true);
|
||||
infoElement.title = titleStr;
|
||||
const label = color_input.previousElementSibling;
|
||||
label.classList.add('theme-color-label');
|
||||
label.appendChild(infoElement);
|
||||
}
|
||||
},
|
||||
|
||||
_in_modal: false,
|
||||
_sidebar_loaded: function(data) {
|
||||
// sidebar was loaded. load the content. check if there are any redirects. call _check_start_editing.
|
||||
|
@ -206,6 +268,7 @@ editor = {
|
|||
if (data !== undefined) {
|
||||
var doc = (new DOMParser).parseFromString(data, 'text/html');
|
||||
content[0].replaceChildren(...doc.body.children);
|
||||
editor.sidebar_content_loaded();
|
||||
}
|
||||
|
||||
var redirect = content.find('span[data-redirect]');
|
||||
|
|
|
@ -73,6 +73,15 @@
|
|||
<table></table>
|
||||
</div>
|
||||
|
||||
<div class="theme-editor-filter">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default active" data-all>{% trans 'All' %}</button>
|
||||
<button type="button" class="btn btn-default" data-base-theme>{% trans 'In base theme' %}</button>
|
||||
<button type="button" class="btn btn-default" data-any-theme>{% trans 'In any theme' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="theme-color-info">{% trans 'Other theme colors' %}</span>
|
||||
|
||||
{% include 'site/fragment_fakemobileclient.html' %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static 'jquery/jquery.js' %}"></script>
|
||||
|
|
|
@ -57,6 +57,7 @@ urlpatterns = [
|
|||
urlpatterns.extend(add_editor_urls('Level', with_list=False, explicit_edit=True))
|
||||
urlpatterns.extend(add_editor_urls('LocationGroupCategory'))
|
||||
urlpatterns.extend(add_editor_urls('LocationGroup'))
|
||||
urlpatterns.extend(add_editor_urls('ObstacleGroup'))
|
||||
urlpatterns.extend(add_editor_urls('DynamicLocation'))
|
||||
urlpatterns.extend(add_editor_urls('WayType'))
|
||||
urlpatterns.extend(add_editor_urls('GroundAltitude'))
|
||||
|
@ -64,6 +65,7 @@ urlpatterns.extend(add_editor_urls('AccessRestriction'))
|
|||
urlpatterns.extend(add_editor_urls('AccessRestrictionGroup'))
|
||||
urlpatterns.extend(add_editor_urls('Source'))
|
||||
urlpatterns.extend(add_editor_urls('LabelSettings'))
|
||||
urlpatterns.extend(add_editor_urls('Theme'))
|
||||
urlpatterns.extend(add_editor_urls('Building', 'Level'))
|
||||
urlpatterns.extend(add_editor_urls('Space', 'Level', explicit_edit=True))
|
||||
urlpatterns.extend(add_editor_urls('Door', 'Level'))
|
||||
|
|
|
@ -51,6 +51,7 @@ def main_index(request):
|
|||
'child_models': [
|
||||
child_model(request, 'LocationGroupCategory'),
|
||||
child_model(request, 'LocationGroup'),
|
||||
child_model(request, 'ObstacleGroup'),
|
||||
child_model(request, 'GroundAltitude'),
|
||||
child_model(request, 'DynamicLocation'),
|
||||
child_model(request, 'WayType'),
|
||||
|
@ -58,6 +59,7 @@ def main_index(request):
|
|||
child_model(request, 'AccessRestrictionGroup'),
|
||||
child_model(request, 'LabelSettings'),
|
||||
child_model(request, 'Source'),
|
||||
child_model(request, 'Theme'),
|
||||
],
|
||||
}, fields=('can_create_level', 'child_models'))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue