#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2008-2016 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # See the file COPYING for details. """ Extra widgets and fields """ from json import JSONEncoder from django import forms from django.conf import settings from django.contrib.gis.db import models from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse from django.forms.widgets import RadioSelect, RadioFieldRenderer, flatatt from django.utils.html import conditional_escape, escape from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from django.template.loader import render_to_string import re BASE_CSS = { "all": ("{}ol3/ol.css".format(settings.STATIC_URL), "{}ol3-layerswitcher/ol3-layerswitcher.css".format( settings.STATIC_URL), "{}chimere/css/forms.css".format(settings.STATIC_URL)) } BASE_JS = tuple( ["{}ol3/ol.js".format(settings.STATIC_URL), '{}jquery/jquery.min.js'.format(settings.STATIC_URL), "{}chimere/js/jquery.chimere.js".format(settings.STATIC_URL), "{}ol3-layerswitcher/ol3-layerswitcher.js".format(settings.STATIC_URL)]) AREA_JS = tuple( ["{}ol3/ol.js".format(settings.STATIC_URL)] + ["{}chimere/js/edit_area.js".format(settings.STATIC_URL), "{}chimere/js/base.js".format(settings.STATIC_URL)]) def getMapJS(area_name=''): """ Variable initialization for drawing the map """ # projection, center and bounds definitions js = "var epsg_display_projection = 'EPSG:%d';\n" \ % settings.CHIMERE_EPSG_DISPLAY_PROJECTION js += "var epsg_projection = 'EPSG:%d';\n" % \ settings.CHIMERE_EPSG_PROJECTION js += "var centerLonLat = ol.proj.transform("\ "[%f, %f], epsg_display_projection, epsg_projection);\n" % \ settings.CHIMERE_DEFAULT_CENTER js += "var media_path = '%s';\n" % settings.MEDIA_URL js += "var static_path = '%s';\n" % settings.STATIC_URL js += "var map_layer = %s;\n" % settings.CHIMERE_DEFAULT_MAP_LAYER js += "var restricted_extent;\n" if area_name: js += "var area_name='%s';\n" % area_name js = "\n" % js return js def get_map_layers(area_name='', get_area_zoom=False): from chimere.models import Area area = None if area_name: try: area = Area.objects.get(urn=area_name) except ObjectDoesNotExist: pass else: try: area = Area.objects.get(default=True) except ObjectDoesNotExist: pass map_layers, default = [], None if area and area.layers.count(): map_layers = [ [layer.name, layer.layer_code, False, layer.extra_js_code or ''] for layer in area.layers.order_by('arealayers__order').all()] def_layer = area.layers.filter(arealayers__default=True) if def_layer.count(): def_layer = def_layer.all()[0] for order, map_layer in enumerate(map_layers): if map_layer[1] == def_layer.layer_code: default = order map_layers[order][2] = True else: map_layers[0][2] = True elif settings.CHIMERE_DEFAULT_MAP_LAYER: map_layers = [(_("Default layer"), settings.CHIMERE_DEFAULT_MAP_LAYER, True, '')] else: map_layers = [("OSM", """new ol.layer.Tile({ style: 'Road', source: new ol.source.OSM() })""", True, '')] if not get_area_zoom: return map_layers, default if not area: return map_layers, default, settings.CHIMERE_DEFAULT_ZOOM zoom = "[%s]" % ",".join(area.getExtent()) return map_layers, default, zoom class ChosenSelectWidget(forms.Select): """ Chosen select widget. """ class Media: js = ["%schosen/chosen/chosen.jquery.min.js" % settings.STATIC_URL] css = { 'all': ["%schosen/chosen/chosen.css" % settings.STATIC_URL] } def render(self, *args, **kwargs): if 'attrs' not in kwargs: kwargs['attrs'] = {} kwargs['attrs'].update({'class': 'chzn-select'}) rendered = super(ChosenSelectWidget, self).render(*args, **kwargs) rendered += "\n\n" % kwargs['attrs']['id'] return mark_safe(rendered) # JQuery UI button select widget. class ButtonRadioInput(RadioSelect): def tag(self, name, value, idx=''): selected = str(value) == str(self.choices[0][0]) if idx: idx = ' id="{}"'.format(idx) return ''.format( name, self.choices[0][0], idx, ' selected="selected"' if selected else '') def render(self, name=None, value=None, attrs=None, choices=(), index=0): attrs = attrs or self.attrs idx = '' if 'id' in self.attrs: idx = '{}_{}'.format(self.attrs['id'], index) label_for = ' for="{}"'.format(idx) if str(value) == str(self.choices[0][0]): label_for += " aria-pressed='true'" else: label_for = '' choice_label = conditional_escape(str(self.choices[0][1])) return mark_safe('%s %s' % ( self.tag(name, value, idx), label_for, choice_label)) class ButtonRadioFieldRenderer(RadioFieldRenderer): def __iter__(self): for i, choice in enumerate(self.choices): yield ButtonRadioInput(self.attrs.copy(), [choice]) def render(self): return mark_safe('\n'.join([ w.render(self.name, self.value, index=idx) for idx, w in enumerate(self)])) class ButtonSelectWidget(forms.RadioSelect): def __init__(self, *args, **kwargs): self.renderer = ButtonRadioFieldRenderer super(ButtonSelectWidget, self).__init__(*args, **kwargs) def render(self, *args, **kwargs): rendered = "
\n" % kwargs['attrs']['id'] rendered += super(ButtonSelectWidget, self).render(*args, **kwargs) """rendered += "\n" % kwargs['attrs']['id'] """ rendered += "\n
\n" return mark_safe(rendered) class ImporterChoicesWidget(forms.Select): """ Importer select widget. """ class Media: js = ["%schimere/js/importer_interface.js" % settings.STATIC_URL] TINYMCE_JS, FULL_TINY_JS, ADMIN_TINY_JS = [], [], [] if settings.ENABLE_TINYMCE: TINYMCE_JS = ["{}tinymce/tinymce.min.js".format(settings.STATIC_URL)] FULL_TINY_JS = TINYMCE_JS[:] + \ ["%schimere/js/textareas.js" % settings.STATIC_URL] ADMIN_TINY_JS = TINYMCE_JS[:] + \ ["%schimere/js/textareas_admin.js" % settings.STATIC_URL] TINYMCE_LANGUAGES = ['fr_FR'] class TextareaWidgetBase(forms.Textarea): """ Manage the edition of a text using TinyMCE """ def render(self, *args, **kwargs): if not TINYMCE_JS: rendered = super(TextareaWidgetBase, self).render(*args, **kwargs) return mark_safe(rendered) if 'attrs' not in kwargs: kwargs['attrs'] = {} if 'class' not in kwargs['attrs']: kwargs['attrs']['class'] = '' else: kwargs['attrs']['class'] += ' ' kwargs['attrs']['class'] += 'mceEditor' rendered = super(TextareaWidgetBase, self).render(*args, **kwargs) rendered += """ """ % args[0] current_language = "" if settings.LANGUAGE_CODE.replace('-', '_') in TINYMCE_LANGUAGES: current_language = settings.LANGUAGE_CODE.replace('-', '_') elif settings.LANGUAGE_CODE.split('-')[0] in TINYMCE_LANGUAGES: current_language = settings.LANGUAGE_CODE.split('-')[0] if current_language: rendered += """ """.format(current_language) return mark_safe(rendered) class FullTextareaWidget(TextareaWidgetBase): """ Manage the edition of a text using TinyMCE """ class Media: js = TINYMCE_JS def render(self, *args, **kwargs): if not TINYMCE_JS: rendered = super(FullTextareaWidget, self).render(*args, **kwargs) return mark_safe(rendered) if 'attrs' not in kwargs: kwargs['attrs'] = {} if 'class' not in kwargs['attrs']: kwargs['attrs']['class'] = '' else: kwargs['attrs']['class'] += ' ' kwargs['attrs']['class'] += 'mceEditor' rendered = super(FullTextareaWidget, self).render(*args, **kwargs) return mark_safe(rendered) class TextareaWidget(TextareaWidgetBase): """ Manage the edition of a text using TinyMCE """ class Media: js = FULL_TINY_JS class TextareaAdminWidget(TextareaWidgetBase): class Media: js = ADMIN_TINY_JS class DatePickerWidget(forms.TextInput): """ Manage the edition of dates. JQuery and Jquery-UI are already loaded by default so don't include them in Media files. """ def render(self, *args, **kwargs): rendered = super(DatePickerWidget, self).render(*args, **kwargs) rendered += "\n\n" % kwargs['attrs']['id'] return mark_safe(rendered) class JQueryAutoComplete(forms.TextInput): TEMPLATE = "chimere/blocks/JQueryAutoComplete.html" def __init__(self, slug, options={}, attrs={}): """ Source can be a list containing the autocomplete values or a string containing the url used for the request. """ self.options = None self.attrs = {} self.slug = slug if len(options) > 0: self.options = JSONEncoder().encode(options) self.attrs.update(attrs) def get_source(self): # Strange... to be fixed source = reverse('chimere:property-choices', kwargs={'property_slug': self.slug}) return "'{}'".format(source) def render(self, name, value=None, attrs=None): attrs_hidden = self.build_attrs(attrs, name=name) attrs_select = self.build_attrs(attrs) selected_value, rendered_value = "", "" if value: val = escape(str(value)) attrs_hidden['value'] = val attrs_select['value'] = val selected_value = val if val: from chimere.models import PropertyModelChoice try: attrs_select['value'] = str( PropertyModelChoice.objects.get( pk=value, propertymodel__slug=self.slug)) rendered_value = attrs_select['value'] except: attrs_select['value'] = "" if 'id' not in self.attrs: attrs_hidden['id'] = 'id_%s' % name attrs_select['id'] = 'id_select_%s' % name if 'class' not in attrs_select: attrs_select['class'] = 'autocomplete' dct = { 'attrs_select': flatatt(attrs_select), 'attrs_hidden': flatatt(attrs_hidden), 'field_id': name, 'min_field_id': name.replace('_', ''), 'options': self.options, 'source': self.get_source(), 'selected_value': selected_value, 'rendered_value': rendered_value } return mark_safe( render_to_string(self.TEMPLATE, dct)) class NominatimWidget(forms.TextInput): class Media: js = ["%schimere/js/nominatim-widget.js" % settings.STATIC_URL] def render(self, name, value, attrs=None, area_name=''): dct = {'id': name, 'nominatim_url': settings.NOMINATIM_URL, 'label': _("Street, City, Country")} tpl = """ """ % dct return mark_safe(tpl) class PointChooserWidget(forms.TextInput): """ Manage the edition of point on a map """ class Media: css = BASE_CSS js = BASE_JS def render(self, name, value, attrs=None, area_name='', initialized=True): """ Render a map and latitude, longitude information field """ val = '' value_x, value_y = 0, 0 if value: val = str(value) if hasattr(value, 'x') and hasattr(value, 'y'): value_x, value_y = value.x, value.y elif isinstance(value, str) and value.startswith('POINT('): try: value_x, value_y = value.split('(')[1][:-1].split(' ') value_x, value_y = float(value_x), float(value_y) except: value = None else: value = None map_layers, default_area, zoom = get_map_layers(area_name, get_area_zoom=True) extra_js = [extra_js for n, js, default, extra_js in map_layers if extra_js] map_layers = [js for n, js, default, xtra_js in map_layers if js] # TODO: manage area return mark_safe( render_to_string('chimere/blocks/live_coordinates.html', {'lat': _("Latitude"), 'value_y': value_y, 'lon': _("Longitude"), 'value_x': value_x, 'name': name, 'val': val, 'initialized': initialized, 'extra_js': "\n".join(extra_js), 'isvalue': bool(value), 'default_area': "true" if default_area else "false", }) % (settings.CHIMERE_EPSG_DISPLAY_PROJECTION, settings.CHIMERE_EPSG_PROJECTION, "[{}, {}]".format(settings.CHIMERE_DEFAULT_CENTER[0], settings.CHIMERE_DEFAULT_CENTER[1]), settings.CHIMERE_DEFAULT_ZOOM, ", ".join(map_layers), zoom ) ) class HiddenPointChooserWidget(PointChooserWidget): """ OpenLayers doesn't initialize well on an hidden field so specific JS must be loaded. """ def render(self, *args, **kwargs): kwargs['initialized'] = False return super(HiddenPointChooserWidget, self).render(*args, **kwargs) class PointField(models.PointField, forms.Field): """ Set the widget for the form field """ def __init__(self, *args, **kwargs): self.widget = kwargs.pop('widget') if 'widget' in kwargs \ else PointChooserWidget return super(PointField, self).__init__(*args, **kwargs) def formfield(self, **keys): defaults = {'widget': self.widget} keys.update(defaults) return super(PointField, self).formfield(**keys) def clean(self, value, instance=None): if len(value) != 2 and self.required: raise forms.ValidationError(_("Invalid point")) return value class RouteChooserWidget(forms.TextInput): """ Manage the edition of route on a map """ class Media: css = BASE_CSS js = BASE_JS def render(self, name, value, attrs=None, area_name='', routefile_id=None, initialized=True): # bad initialization if value == 'None': value = '' val = '' if value: val = str(value) map_layers, default_area, zoom = get_map_layers(area_name, get_area_zoom=True) extra_js = [extra_js for n, js, default, extra_js in map_layers if extra_js] map_layers = [js for n, js, default, ext_js in map_layers if 'OpenLayers' not in js] tpl = render_to_string( 'chimere/blocks/edit_widget.html', {'name': name, 'val': val, 'initialized': initialized, 'isvalue': bool(value), 'default_area': "true" if default_area else "false", 'value': value } ) return mark_safe(tpl.format( static_url=settings.STATIC_URL, display_projection=settings.CHIMERE_EPSG_DISPLAY_PROJECTION, projection=settings.CHIMERE_EPSG_PROJECTION, center=list(settings.CHIMERE_DEFAULT_CENTER), zoom=zoom, edition_type="route", map_layers=", ".join(map_layers), extra_js="\n".join(extra_js), )) class RouteField(models.LineStringField): """ Set the widget for the form field """ def formfield(self, **keys): defaults = {'widget': RouteChooserWidget} keys.update(defaults) return super(RouteField, self).formfield(**keys) class AreaWidget(forms.TextInput): """ Manage the edition of an area on the map """ class Media: css = BASE_CSS js = AREA_JS def get_bounding_box_from_value(self, value): """ Return upper left lat/lon and lower lat/lon from the input value """ upper_left_lat, upper_left_lon = 0, 0 lower_right_lat, lower_right_lon = 0, 0 if not value: return upper_left_lat, upper_left_lon, lower_right_lat, \ lower_right_lon if len(value) == 2: upper_left = value[0] lower_right = value[1] if hasattr(upper_left, 'x') and hasattr(upper_left, 'y'): upper_left_lon, upper_left_lat = upper_left.x, upper_left.y elif len(upper_left) == 2: try: upper_left_lon = float(upper_left[0]) upper_left_lat = float(upper_left[1]) except ValueError: pass if hasattr(lower_right, 'x') and hasattr(lower_right, 'y'): lower_right_lon, lower_right_lat = lower_right.x, \ lower_right.y elif len(lower_right) == 2: lower_right_lon, lower_right_lat = lower_right try: lower_right_lon = float(lower_right[0]) lower_right_lat = float(lower_right[1]) except ValueError: pass return upper_left_lat, upper_left_lon, lower_right_lat, lower_right_lon def render(self, name, value, attrs=None, initialized=True): """ Render a map """ upper_left_lat, upper_left_lon, lower_right_lat, lower_right_lon = \ self.get_bounding_box_from_value(value) tpl = getMapJS() tpl += "\n"\ "\n"\ "\n"\ "\n"\ "\n" % ( upper_left_lat, upper_left_lon, lower_right_lat, lower_right_lon) help_msg = _("Click to begin selecting area on the map and click again " "to close the rectangle. To modify, move the nodes of the rectangle.") tpl += "

%s

\n" % help_msg tpl += "\n" tpl += "
\n" return mark_safe(tpl) def value_from_datadict(self, data, files, name): """ Return the appropriate values """ values = [] for keys in (('upper_left_lon', 'upper_left_lat',), ('lower_right_lon', 'lower_right_lat')): value = [] for key in keys: val = data.get(key, None) if not val: return [] value.append(val) values.append(value) return values def decompress(self, value): if value: return value return [None, None] class PolygonChooserWidget(forms.TextInput): """ Manage the edition of polygon on a map """ class Media: css = BASE_CSS js = BASE_JS def render(self, name, value, attrs=None, area_name='', initialized=True): val = '' if value: val = str(value) map_layers, default_area, zoom = get_map_layers(area_name, get_area_zoom=True) extra_js = [extra_js for n, js, default, extra_js in map_layers if extra_js] map_layers = [js for n, js, default, ext_js in map_layers if 'OpenLayers' not in js] tpl = render_to_string( 'chimere/blocks/edit_widget.html', {'name': name, 'val': val, 'initialized': initialized, 'isvalue': bool(value), 'default_area': "true" if default_area else "false", 'value': value } ) return mark_safe(tpl.format( static_url=settings.STATIC_URL, display_projection=settings.CHIMERE_EPSG_DISPLAY_PROJECTION, projection=settings.CHIMERE_EPSG_PROJECTION, center=list(settings.CHIMERE_DEFAULT_CENTER), zoom=zoom, edition_type="polygon", map_layers=", ".join(map_layers), extra_js="\n".join(extra_js), )) class PolygonField(models.PolygonField): """ Set the widget for the form field """ def formfield(self, **keys): defaults = {'widget': PolygonChooserWidget} keys.update(defaults) return super(PolygonField, self).formfield(**keys) RE_XAPI = re.compile( '(node|way)\[(.*=.*)\]\[bbox=' '(-*[0-9]*.[0-9]*,-*[0-9]*.[0-9]*,-*[0-9]*.[0-9]*,-*[0-9]*.[0-9]*)\]') class ImportFiltrWidget(AreaWidget): """ Manage the edition of the import source field """ class Media: css = BASE_CSS js = AREA_JS def render(self, name, value, attrs=None): """ Render a map """ tpl = super(ImportFiltrWidget, self).render(name, value, attrs, initialized=False) tpl += "

" vals = {'lbl': _("Type:"), 'name': name, 'node': _("Node"), 'way': _("Way")} vals['way_selected'] = ' checked="checked"'\ if self.xapi_type == 'way' else '' vals['node_selected'] = ' checked="checked"'\ if self.xapi_type == 'node' else '' tpl += "
"\ " "\ "
" % vals help_msg = _( "Enter an OSM \"tag=value\" string such as " "\"amenity=pub\". A list of common tag is available " "here.") tpl += "

%s

\n" % help_msg tpl += "
"\ "
" % ( name, _("Tag:"), name, self.xapi_tag) tpl += "\n" help_msg = _("If you change the above form don't forget to refresh " "before submit!") tpl += "

%s

\n" % help_msg help_msg = _("You can put a Folder name of the KML file to filter on " "it.") tpl += "

%s

\n" % help_msg if not value: value = '' tpl += "
" % (name, name, value, name, _("Refresh")) return mark_safe(tpl) def value_from_datadict(self, data, files, name): """ Return the appropriate values """ return data.get('id_' + name, None) def get_bounding_box_from_value(self, value): """ Return upper left lat/lon, lower lat/lon from the input value. Get also xapi type and xapi tag """ upper_left_lat, upper_left_lon = 0, 0 lower_right_lat, lower_right_lon = 0, 0 self.xapi_type, self.xapi_tag, self.bounding_box = None, '', None if not value: return upper_left_lat, upper_left_lon, lower_right_lat, \ lower_right_lon xapi_m = RE_XAPI.match(value) if not xapi_m: return upper_left_lat, upper_left_lon, lower_right_lat, \ lower_right_lon # as the regexp pass, we could be pretty confident self.xapi_type, self.xapi_tag, self.bounding_box = xapi_m.groups() upper_left_lon, lower_right_lat, lower_right_lon, upper_left_lat = \ self.bounding_box.split(',') return float(upper_left_lat), float(upper_left_lon), \ float(lower_right_lat), float(lower_right_lon) class AreaField(forms.MultiValueField): """ Set the widget for the form field """ widget = AreaWidget def compress(self, data_list): if not data_list: return None return data_list class MultiSelectWidget(forms.SelectMultiple): class Media: EXTRA_CSS = [ settings.STATIC_URL + 'chosen/chosen.min.css', ] css = {'all': EXTRA_CSS} EXTRA_JS = [ settings.STATIC_URL + 'chosen/chosen.jquery.min.js', ] js = ['%sjquery/jquery.min.js' % settings.STATIC_URL] + EXTRA_JS def render(self, name, value, attrs=None): rendered = super(MultiSelectWidget, self).render(name, value, attrs) rendered += """
""" % {'name': name, 'title': _("Select...")} return mark_safe(rendered) class SelectMultipleField(models.ManyToManyField): """ Set the widget for the category field """ def formfield(self, **keys): self.help_text = "" defaults = {'widget': MultiSelectWidget} keys.update(defaults) return super(SelectMultipleField, self).formfield(**keys)