diff --git a/src/c3nav/routing/api.py b/src/c3nav/routing/api.py index 6829fb5e..70938318 100644 --- a/src/c3nav/routing/api.py +++ b/src/c3nav/routing/api.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import list_route from rest_framework.response import Response @@ -12,6 +13,10 @@ from c3nav.routing.router import Router class RoutingViewSet(ViewSet): + """ + /route/ Get routes. + /options/ Get or set route options. + """ @list_route(methods=['get', 'post']) def route(self, request, *args, **kwargs): params = request.POST if request.method == 'POST' else request.GET @@ -20,7 +25,15 @@ class RoutingViewSet(ViewSet): if not form.is_valid(): return Response({ 'errors': form.errors, - }) + }, status=400) + + options = RouteOptions.get_for_request(request) + try: + options.update(params, ignore_unknown=True) + except ValidationError as e: + return Response({ + 'errors': (str(e), ), + }, status=400) try: route = Router.load().get_route(origin=form.cleaned_data['origin'], @@ -44,6 +57,7 @@ class RoutingViewSet(ViewSet): 'origin': form.cleaned_data['origin'].pk, 'destination': form.cleaned_data['destination'].pk, }, + 'options': options.serialize(), 'result': route.serialize(locations=visible_locations_for_request(request)), }) @@ -55,13 +69,4 @@ class RoutingViewSet(ViewSet): pass options = RouteOptions.get_for_request(request) - return Response({ - 'options': options.data, - 'fields': { - name: { - 'type': field.widget.input_type, - 'label': field.label, - 'choices': dict(field.choices), - } - for name, field in options.get_fields().items()}, - }) + return Response(options.serialize()) diff --git a/src/c3nav/routing/models.py b/src/c3nav/routing/models.py index d4408526..b6b17a65 100644 --- a/src/c3nav/routing/models.py +++ b/src/c3nav/routing/models.py @@ -116,14 +116,14 @@ class RouteOptions(models.Model): except AttributeError: return self.get_fields()[key].initial - def update(self, value_dict, ignore_errors=False): + def update(self, value_dict, ignore_errors=False, ignore_unknown=False): if not value_dict: return fields = self.get_fields() for key, value in value_dict.items(): field = fields.get(key) if not field: - if ignore_errors: + if ignore_errors or ignore_unknown: continue raise ValidationError(_('Unknown route option: %s') % key) if value is None or value not in dict(field.choices): @@ -135,6 +135,24 @@ class RouteOptions(models.Model): def __setitem__(self, key, value): self.update({key: value}) + def serialize(self): + return [ + { + 'name': name, + 'type': field.widget.input_type, + 'label': field.label, + 'choices': [ + { + 'name': choice_name, + 'title': choice_title, + } + for choice_name, choice_title in field.choices + ], + 'value': self[name], + } + for name, field in self.get_fields().items() + ] + def save(self, *args, **kwargs): if self.request is None or self.request.user.is_authenticated: self.user = self.request.user diff --git a/src/c3nav/site/static/site/css/c3nav.css b/src/c3nav/site/static/site/css/c3nav.css index 2ecb4f3f..bddc9538 100644 --- a/src/c3nav/site/static/site/css/c3nav.css +++ b/src/c3nav/site/static/site/css/c3nav.css @@ -219,7 +219,7 @@ section.details > * { section.details > .details-head { padding: 11px 10px 8px; } -.details-head > .button { +section.details > .details-head > .button { margin: -2px 0 0; transition: none; line-height: 2.5; @@ -304,7 +304,9 @@ section.details { main:not([data-view$=search]) #autocomplete, main:not([data-view=location]) #location-details, main:not([data-view=route-result]) #route-details, -main:not(.show-details) #resultswrapper .details { +main:not([data-view=route-result]) #route-options, +main:not(.show-details) #resultswrapper .details:not(#route-options), +main:not(.show-options) #resultswrapper #route-options { display:none; } main .buttons .details .material-icons { @@ -313,7 +315,8 @@ main .buttons .details .material-icons { main.show-details .buttons .details .material-icons { transform: scale(1, -1); } -main.show-details #resultswrapper .details { +main.show-details #resultswrapper .details, +main.show-options #resultswrapper #route-options { animation: show-details; animation-duration: 150ms; animation-timing-function: ease-out; @@ -328,6 +331,27 @@ main.show-details #resultswrapper .details { top: 0; } } +.route-options-fields { + padding: 0 10px 5px; +} +.route-options-fields input, .route-options-fields select { + margin-bottom: 1rem; +} +.route-options-buttons { + display: flex; + flex-wrap: wrap; + padding: 0 0 15px 10px; +} +.route-options-buttons button { + padding: 0 1rem; + margin: 0 10px 0 0; + flex-grow: 1; +} +#route-options .details-head button { + font-size: 30px; + line-height: 1.0; + color: #b2b2b2; +} .location { position: relative; @@ -519,7 +543,7 @@ main:not([data-view=route-result]) #route-dots { .buttons > *:hover, .buttons > *:active { background-color: #eeeeee; } -main.map button, main.map .button { +#search button, .leaflet-popup button, .details-head .button { font-size: 1.3rem; line-height: 1.3; height: 3.3rem; @@ -563,6 +587,19 @@ main:not([data-view=route-result]) #route-summary { opacity: 0; } +#route-summary button.options { + position: absolute; + top: 8px; + right: 6px; + padding: 0; + width: 37px; + height: 37px; + border-width: 0; + font-size: 36px; + color: #b2b2b2; + line-height: 1; +} + @media not all and (min-height: 700px) and (min-width: 1100px) { main[data-view=route-result] #sidebar #search:not(.focused) .locationinput { margin-bottom: -21px; diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 2e0ca778..a9865192 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -83,6 +83,8 @@ c3nav = { $result_buttons.find('.details').on('click', c3nav._buttons_details_click); $('#route-search-buttons, #route-result-buttons').find('.swap').on('click', c3nav._route_buttons_swap_click); $('#route-search-buttons').find('.close').on('click', c3nav._route_buttons_close_click); + $('#route-summary').find('.options').on('click', c3nav._buttons_options_click); + $('#route-options').find('.close').on('click', c3nav._route_options_close_click); $('#map').on('click', '.location-popup .button-clear', c3nav._popup_button_click); $('#modal').on('click', c3nav._modal_click) @@ -99,9 +101,15 @@ c3nav = { }, state: {}, - update_state: function(routing, replace, details) { + update_state: function(routing, replace, details, options) { if (typeof routing !== "boolean") routing = c3nav.state.routing; + if (details) { + options = false; + } else if (options) { + details = false; + } + var destination = $('#destination-input').data('location'), origin = $('#origin-input').data('location'), new_state = { @@ -109,7 +117,8 @@ c3nav = { origin: origin, destination: destination, sidebar: true, - details: !!details + details: !!details, + options: !!options }; c3nav._push_state(new_state, replace); @@ -147,6 +156,7 @@ c3nav = { if (view === 'route-result') { if (state.route_result) { c3nav._display_route_result(state.route_result, nofly); + c3nav._display_route_options(state.route_options); } else { c3nav.load_route(state.origin, state.destination, nofly); } @@ -155,7 +165,9 @@ c3nav = { c3nav._clear_route_layers(); } - $('main').attr('data-view', view).toggleClass('show-details', state.details); + $('main').attr('data-view', view) + .toggleClass('show-details', !!state.details) + .toggleClass('show-options', !!state.options); var $search = $('#search'); $search.removeClass('loading'); @@ -233,11 +245,13 @@ c3nav = { }, load_route: function (origin, destination, nofly) { var $route = $('#route-summary'), - $details_wrapper = $('#route-details'); + $details_wrapper = $('#route-details'), + $options_wrapper = $('#route-options'); if ($route.attr('data-origin') !== String(origin.id) || $route.attr('data-destination') !== String(destination.id)) { c3nav._clear_route_layers(); $route.addClass('loading').attr('data-origin', origin.id).attr('data-destination', destination.id); $details_wrapper.addClass('loading'); + $options_wrapper.addClass('loading'); $.post('/api/routing/route/', { 'origin': origin.id, 'destination': destination.id, @@ -262,8 +276,9 @@ c3nav = { // loaded too late, information no longer needed return; } - c3nav._push_state({route_result: data.result}, true); + c3nav._push_state({route_result: data.result, route_options: data.options}, true); c3nav._display_route_result(data.result, nofly); + c3nav._display_route_options(data.options); }, _display_route_result: function(result, nofly) { var $route = $('#route-summary'), @@ -415,9 +430,31 @@ c3nav = { new_coords.push(coords[coords.length-1]); return new_coords }, + _display_route_options: function(options) { + var $options_wrapper = $('#route-options'), + $options = $options_wrapper.find('.route-options-fields'), + option, field, field_id, choice; + $options.html(''); + for (var i=0; i').text(option.label)); + if (option.type === 'select') { + field = $('