#!/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 import forms as gis_forms 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 from django.utils.html import conditional_escape, escape from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from django.forms.utils import flatatt 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, renderer=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 PointFormField(gis_forms.PointField): pass 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('POINT({} {})'.format(value[0], value[1])) 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)