#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2017 Étienne Loks # Copyright (C) 2007 skam # (http://djangosnippets.org/snippets/233/) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # See the file COPYING for details. import logging from django import forms from django.conf import settings from django.contrib.gis import forms as gis_forms from django.core.exceptions import ValidationError from django.core.files import File from django.db.models import fields from django.forms import ClearableFileInput from django.forms.utils import flatatt from django.forms.widgets import ( CheckboxSelectMultiple as CheckboxSelectMultipleBase, NumberInput, ) from django.template import loader from django.template.defaultfilters import slugify from django.urls import reverse, NoReverseMatch from django.utils.encoding import smart_text from django.utils.functional import lazy from django.utils.html import escape from django.utils.safestring import mark_safe from json import JSONEncoder from django.utils.translation import ugettext_lazy as _ from ishtar_common import models from ishtar_common.utils import get_columns_from_class, reverse_coordinates logger = logging.getLogger(__name__) reverse_lazy = lazy(reverse, str) class SelectReadonly(forms.Select): template_name = "blocks/readonly_input.html" option_template_name = "blocks/readonly_input_option.html" def __init__(self, attrs=None, choices=(), model=None, available=None): super(SelectReadonly, self).__init__(attrs, choices) self.available = available self.model = model def get_choices(self, value): q = self.model.objects if self.available: q = q.filter(available=True) if value: q = q.filter(pk=value) for i in q.all(): if hasattr(self.model, "verbose_name"): label = i.verbose_name else: label = str(i) yield (i.pk, label) def render(self, name, value, attrs=None, choices=(), renderer=None): if value: self.choices = list(self.get_choices(value)) value = self.choices[0][0] return super(SelectReadonly, self).render(name, value, attrs, renderer=None) class SelectReadonlyField(forms.ChoiceField): def __init__( self, choices=(), required=True, widget=None, label=None, initial=None, help_text="", *args, **kwargs ): self.available = False self.model = None if "model" in kwargs: self.model = kwargs.pop("model") if "available" in kwargs: self.available = kwargs.pop("available") widget = SelectReadonly(model=self.model, available=self.available) super(SelectReadonlyField, self).__init__( choices=choices, required=required, widget=widget, label=label, initial=initial, help_text=help_text, *args, **kwargs ) def get_q(self): q = self.model.objects if self.available: q = q.filter(available=True) return q def valid_value(self, value): if not self.model: return super(SelectReadonlyField, self).valid_value(value) return bool(self.get_q().filter(pk=value).count()) class Select2Media: @property def media(self): css = {"all": ("select2/css/select2.css",)} js = ["select2/js/select2.full.min.js"] for lang_code, lang in settings.LANGUAGES: js.append("select2/js/i18n/{}.js".format(lang_code)) media = forms.Media(css=css, js=js) return media class Select2DynamicBase(Select2Media): """ Select input using select, allowing dynamic creation. """ MULTIPLE = False def render(self, name, value, attrs=None, choices=(), renderer=None): choices = choices or getattr(self, "choices", []) if value: values = [value] if self.MULTIPLE: value = value[0] values = value for va in values: if va not in [key for key, v in choices]: choices.insert(1, (va, va)) self.choices = choices klass = attrs and attrs.get("class") or "" klass += " " if klass else "" + "js-select2" if not attrs: attrs = {} attrs["class"] = klass if "style" not in attrs: if attrs.get("full-width", None): attrs["style"] = "width: calc(100% - 60px)" else: attrs["style"] = "width: 370px" options = [ "tags: true", ] ''' msg = str( _("Are you sure you want to add this term? (the addition is " "effective after registration of the element)") ) options.append("""createTag: function (params) {{ return confirm("{}"); }}""".format(msg)) ''' if attrs.get("full-width", None): options.append("containerCssClass: 'full-width'") if self.MULTIPLE: options.append("multiple: 'true'") html = super(Select2DynamicBase, self).render(name, value, attrs, renderer=None) html += """ """.format( name, ", ".join(options) ) return mark_safe(html) class Select2Dynamic(Select2DynamicBase, forms.Select): MULTIPLE = False class Select2DynamicMultiple(Select2DynamicBase, forms.SelectMultiple): MULTIPLE = True class Select2DynamicField(forms.ChoiceField): widget = Select2Dynamic def validate(self, value): """ Key can be added dynamically. Only check that the character " is not used. """ if value and '"' in value: raise ValidationError(_('The character " is not accepted.')) def to_python(self, value): """ Strip value """ return super(Select2DynamicField, self).to_python(value).strip() class Select2DynamicMultipleField(forms.MultipleChoiceField): widget = Select2DynamicMultiple def validate(self, value): """ Key can be added dynamically. Only check that the character " is not used. """ if value and '"' in value: raise ValidationError(_('The character " is not accepted.')) def to_python(self, value): """ Strip value """ return [v.strip() for v in super().to_python(value)] class Select2Base(Select2Media): def __init__( self, attrs=None, choices=(), remote=None, model=None, new=None, available=None ): self.remote = remote self.available = available self.model = model self.new = new super(Select2Base, self).__init__(attrs, choices) def get_q(self): q = self.model.objects if self.available: q = q.filter(available=True) return q def get_choices(self): for i in self.get_q().all(): yield (i.pk, str(i)) def render(self, name, value, attrs=None, choices=(), renderer=None): self.remote = str(self.remote) if self.remote in ("None", "false"): # test on lazy object is buggy... so we have this ugly test self.remote = None if not choices: if not self.remote and self.model: choices = self.get_choices() if hasattr(self, "choices") and self.choices: choices = self.choices new_attrs = self.attrs.copy() new_attrs.update(attrs) attrs = new_attrs klass = attrs and attrs.get("class") or "" klass += " " if klass else "" + "js-select2" if not attrs: attrs = {} attrs["class"] = klass if "style" not in attrs: if attrs.get("full-width", None): attrs["style"] = "width: calc(100% - 60px)" else: attrs["style"] = "width: 370px" if value: if type(value) not in (list, tuple): value = value.split(",") options = "" if self.remote: options = ( """{ ajax: { url: '%s', delay: 250, dataType: 'json', minimumInputLength: 2, processResults: function (data) { if(!data) return {results: []}; var result = $.map(data, function (item) { return { text: item['value'], id: item['id'] } }); return { results: result } } } }""" % self.remote ) if value: choices = [] for v in value: try: choices.append((v, self.model.objects.get(pk=v))) except (self.model.DoesNotExist, ValueError): # an old reference? it should not happen pass if attrs.get("full-width", None): if options: options = options[:-1] + ", " else: options = "{" options += " containerCssClass: 'full-width'}" self.choices = choices new, html = "", "" if self.new: html = "
" url_new = "new-" + self.model.SLUG url_new = reverse(url_new, args=["id_" + name]) # WARNING: the modal for the form must be in the main template # "extra_form_modals" list is used for that in form or view new = ( """""" """""" """+
""".format(url_new, self.model.SLUG) ) html += super(Select2Base, self).render(name, value, attrs, renderer=None) html += new html += """ """.format( name, options ) return mark_safe(html) class Select2Simple(Select2Base, forms.Select): pass class Select2Multiple(Select2Base, forms.SelectMultiple): pass class CheckboxSelectMultiple(CheckboxSelectMultipleBase): """ Fix initialization bug. Should be corrected on recent Django version. TODO: test and remove (test case: treatment type not keep on modif) """ def render(self, name, value, attrs=None, choices=(), renderer=None): if type(value) in (str, str): value = value.split(",") if not isinstance(value, (list, tuple)): value = [value] return super(CheckboxSelectMultiple, self).render( name, value, attrs, renderer=None ) class Select2BaseField(object): multiple = False def __init__(self, *args, **kwargs): new = None if "new" in kwargs: new = kwargs.pop("new") remote = None if "remote" in kwargs: remote = kwargs.pop("remote") self.model, self.remote = None, None if "model" in kwargs: self.model = kwargs.pop("model") if remote: self.remote = reverse_lazy( "autocomplete-" + self.model.__name__.lower() ) long_widget = False if "long_widget" in kwargs: long_widget = kwargs.pop("long_widget") self.available = False if "available" in kwargs: self.available = kwargs.pop("available") attrs = {} if long_widget: attrs["cols"] = True attrs["full-width"] = True if self.multiple: widget = Select2Multiple else: widget = Select2Simple if kwargs.get("style", None): attrs["style"] = kwargs.pop("style") kwargs["widget"] = widget( model=self.model, available=self.available, remote=self.remote, new=new, attrs=attrs, ) super(Select2BaseField, self).__init__(*args, **kwargs) def get_q(self): q = self.model.objects if self.available: q = q.filter(available=True) return q def valid_value(self, value): if not self.model: return super(Select2BaseField, self).valid_value(value) return bool(self.get_q().filter(pk=value).count()) class Select2MultipleField(Select2BaseField, forms.MultipleChoiceField): multiple = True def to_python(self, value): if not isinstance(value, (list, tuple)): if value: value = value.split(",") else: value = [] return super(Select2MultipleField, self).to_python(value) class Select2SimpleField(Select2BaseField, forms.ChoiceField): pass class DeleteWidget(forms.CheckboxInput): def render(self, name, value, attrs=None, renderer=None): final_attrs = flatatt( self.build_attrs( attrs, {"name": name, "value": "1", "class": "btn btn-danger"} ) ) output = "%s" % (final_attrs, _("Delete")) return mark_safe(output) class SwitchWidget(forms.CheckboxInput): extra_class = "" extra_label = "" def render(self, name, value, attrs=None, renderer=None): extra_class = (" " + self.extra_class) if self.extra_class else "" default = { "name": name, "value": "1", "class": "switch" + extra_class, "type": "checkbox", } if value: default["checked"] = "checked" attrs = self.build_attrs(attrs, default) final_attrs = flatatt(attrs) extra_label = "" if self.extra_label: extra_label = ''.format( attrs["id"], self.extra_label ) output = """ {} """.format( extra_class, final_attrs, extra_label ) return mark_safe(output) class DeleteSwitchWidget(SwitchWidget): extra_class = "danger" extra_label = _("Delete") class BSClearableFileInput(ClearableFileInput): template_name = "widgets/clearable_file_input.html" class ImageFileInput(ClearableFileInput): template_name = "widgets/image_input.html" NO_FORM_CONTROL = True def format_value(self, value): if self.is_initial(value): return value # try to display posted images try: has_file = hasattr(value, "file") except ValueError: has_file = False if has_file: if hasattr(value, "file"): full_path = str(value.file) if full_path.startswith(settings.MEDIA_ROOT): value.url = ( settings.MEDIA_URL + full_path[len(settings.MEDIA_ROOT) :] ) elif value: full_path = settings.MEDIA_ROOT + str(value) try: with open(full_path) as f: f = File(f) f.url = settings.MEDIA_URL + str(value) value = f except IOError: return value return value def get_context(self, name, value, attrs): context = super(ImageFileInput, self).get_context(name, value, attrs) if getattr(self, "hidden", None): context["hidden_value"] = self.hidden # on post memory file is used: display the name if getattr(self, "hidden_name", None): context["hidden_name_value"] = self.hidden_name return context def value_from_datadict(self, data, files, name): value = super(ImageFileInput, self).value_from_datadict(data, files, name) hidden_name = name + "-hidden" hidden_name_value = name + "-hidden-name" self.hidden, self.hidden_name = None, None if name in files: # new file posted self.hidden_name = files.get(name).name elif data.get(hidden_name_value, None): # file posted previously - keep the name self.hidden_name = data.get(hidden_name_value) elif hidden_name in data: # initial file self.hidden = data.get(hidden_name) return value class CustomWidget(forms.TextInput): TEMPLATE = "" EXTRA_DCT = {} def render(self, name, value, attrs=None, renderer=None): if not value: value = "" final_attrs = flatatt(self.build_attrs(attrs, {"name": name, "value": value})) dct = { "final_attrs": final_attrs, "id": attrs["id"], "safe_id": attrs["id"].replace("-", "_"), } dct.update(self.EXTRA_DCT) t = loader.get_template(self.TEMPLATE) rendered = t.render(dct) return mark_safe(rendered) class SquareMeterWidget(CustomWidget): TEMPLATE = "widgets/UnitWidget.html" EXTRA_DCT = {"unit1": "m²", "unit2": "ha", "factor": 10000} class GramKilogramWidget(CustomWidget): TEMPLATE = "widgets/UnitWidget.html" EXTRA_DCT = {"unit1": "g", "unit2": "kg", "factor": 1000} class CentimeterMeterWidget(CustomWidget): TEMPLATE = "widgets/UnitWidget.html" EXTRA_DCT = {"unit1": "cm", "unit2": "m", "factor": 100} class MeterKilometerWidget(CustomWidget): TEMPLATE = "widgets/UnitWidget.html" EXTRA_DCT = {"unit1": "m", "unit2": "km", "factor": 1000} class MeterCentimeterWidget(CustomWidget): TEMPLATE = "widgets/UnitWidget.html" EXTRA_DCT = {"unit1": "m", "unit2": "cm", "factor": "0.01"} AreaWidget = forms.TextInput if settings.SURFACE_UNIT == "square-metre": AreaWidget = SquareMeterWidget class ISBNWidget(CustomWidget): TEMPLATE = "widgets/CheckTextWidget.html" EXTRA_DCT = {"validator": "is_valid_isbn"} class ISSNWidget(CustomWidget): TEMPLATE = "widgets/CheckTextWidget.html" EXTRA_DCT = {"validator": "is_valid_issn"} class CheckboxInput(forms.CheckboxInput): NO_FORM_CONTROL = True class SearchWidget(forms.TextInput): template_name = "widgets/search_input.html" def __init__(self, app_name=None, model=None, pin_model=None, attrs=None): super(SearchWidget, self).__init__(attrs) self.app_name = app_name self.model = model if not pin_model: pin_model = self.model self.pin_model = pin_model def get_context(self, name, value, attrs): context = super(SearchWidget, self).get_context(name, value, attrs) context["app_name"] = self.app_name context["model"] = self.model context["pin_model"] = self.pin_model return context class ModelFieldMixin(object): def to_python(self, value): if not value: return if not self.multiple: value = [value] values = [] for v in value: if not v: continue try: values.append(self.model.objects.get(pk=v)) except self.model.DoesNotExist: raise ValidationError( str(_("{} is not a valid key for {}")).format(v, self.model) ) if not self.multiple: return values[0] return values class ModelChoiceField(ModelFieldMixin, forms.ChoiceField): def __init__(self, model, multiple=False, *args, **kwargs): self.model = model self.multiple = multiple super(ModelFieldMixin, self).__init__(*args, **kwargs) def valid_value(self, value): if value and getattr(value, "pk", None) in [v for v, l in self.choices]: return True return super(ModelChoiceField, self).valid_value(value) class ModelJQueryAutocompleteField(ModelFieldMixin, forms.CharField): def __init__( self, model, multiple=False, new=False, long_widget=False, *args, **kwargs ): self.model = model self.multiple = multiple attrs = {} if long_widget: attrs["cols"] = True attrs["full-width"] = True kwargs["widget"] = JQueryAutoComplete( reverse_lazy("autocomplete-" + self.model.SLUG), associated_model=self.model, new=new, multiple=multiple, attrs=attrs, ) super(ModelJQueryAutocompleteField, self).__init__(*args, **kwargs) class JQueryAutoComplete(forms.TextInput): def __init__( self, source, associated_model=None, options=None, attrs=None, new=False, url_new="", multiple=False, limit=None, dynamic_limit=None, detail=False, modify=False, tips="", ): """ Source can be a list containing the autocomplete values or a string containing the url used for the request. """ self.source = source self.associated_model = associated_model self.tips = tips self.options = None if options and len(options) > 0: self.options = JSONEncoder().encode(options) self.attrs = {} if attrs: self.attrs.update(attrs) self.new = new self.url_new = url_new self.modify = modify self.detail = detail self.multiple = multiple self.limit = limit or {} self.dynamic_limit = dynamic_limit or [] def value_from_datadict(self, data, files, name): v = data.get(name, None) if not self.multiple: return data.get(name, None) if type(v) == str and "," in v: return [item.strip() for item in v.split(",") if item.strip()] return data.getlist(name, None) def render_js(self, field_id, current_pk): if isinstance(self.source, list): source = JSONEncoder().encode(self.source) elif isinstance(self.source, str) or isinstance(self.source, str): source = "'%s'" % escape(self.source) else: try: source = "'" + str(self.source) + "'" except: raise ValueError("{} source type is not valid".format(self.source)) dynamic_limit = [] for lim in self.dynamic_limit: field_ids = field_id.split("-") if field_ids[1:-1]: dynamic_limit.append( "id_" + lim.replace("_", "") + "-" + "-".join(field_ids[1:-1]) + "-" + lim ) else: dynamic_limit.append("id_" + lim.replace("_", "")) dct = { "source": mark_safe(source), "field_id": field_id, "safe_field_id": field_id.replace("-", "_"), "modify": self.modify, "dynamic_limit": dynamic_limit, } if self.associated_model: model_name = self.associated_model._meta.object_name.lower() dct["model_name"] = model_name if self.detail: model_name = self.associated_model._meta.object_name.lower() url_detail = "/detail-{}/".format(model_name) dct["detail"] = url_detail if self.options: dct["options"] = mark_safe("%s" % self.options) tpl = "blocks/JQueryAutocomplete.js" if self.multiple: tpl = "blocks/JQueryAutocompleteMultiple.js" t = loader.get_template(tpl) js = t.render(dct) return js def render(self, name, value, attrs=None, renderer=None): attrs_hidden = self.build_attrs(attrs, {"name": name}) attrs_select = self.build_attrs(attrs) attrs_select["placeholder"] = _("Search...") values = [] if value: hiddens = [] selects = [] if type(value) not in (list, tuple): values = str(escape(smart_text(value))) values = values.replace("[", "").replace("]", "") values = values.split(",") else: values = [] for v in value: values += v.split(",") for v in values: if not v: continue hiddens.append(v) selects.append(v) if self.associated_model: try: selects[-1] = str(self.associated_model.objects.get(pk=v)) except (self.associated_model.DoesNotExist, ValueError): selects.pop() hiddens.pop() if self.multiple: attrs_hidden["value"] = ", ".join(hiddens) if selects: selects.append("") attrs_select["value"] = ", ".join(selects) else: if hiddens and selects: attrs_hidden["value"] = hiddens[0] attrs_select["value"] = selects[0] 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" has_previous_value = "value" in attrs_select and attrs_select["value"] attrs_select["class"] += " form-control" new = "" html = "" if self.tips or self.new or self.modify: klass = "input-group" if has_previous_value: klass += " has-previous-value" html = "
".format(klass) # WARNING: the modal for the form must be in the main template # "extra_form_modals" list is used for that in form or view model_name = self.associated_model._meta.object_name.lower() if self.tips: tips = self.tips if callable(tips): tips = tips() new += """ {} """.format( attrs_hidden["id"], tips ) if self.modify: new += """ """.format( attrs_hidden["id"], name.replace("-", "_") ) if self.new: limits = [] for k in self.limit: limits.append(k + "__" + "-".join([str(v) for v in self.limit[k]])) args = [attrs_select["id"]] if limits: args.append(";".join(limits)) url_new = "new-" + model_name if self.url_new: url_new = self.url_new url_new = reverse(url_new, args=args) new += """ + """.format( url_new, model_name, model_name ) new += "
" detail = "" if self.detail: detail = """
""".format( attrs_hidden["id"] ) old_value = "" if has_previous_value: old_value = """
{}
""".format( _("Prev.:"), attrs_hidden["id"] + "_previous_label", attrs_select["value"], attrs_hidden["id"] + "_previous_button", _("Restore previous"), ) attrs_hidden_previous = attrs_hidden.copy() attrs_hidden_previous["name"] += "_previous" attrs_hidden_previous["id"] += "_previous" old_value += "".format( flatatt(attrs_hidden_previous) ) pk = None if values: pk = values[0] html += """ {new}\ \ {detail}{old_value} """.format( old_value=old_value, attrs_select=flatatt(attrs_select), attrs_hidden=flatatt(attrs_hidden), js=self.render_js(name, pk), new=new, detail=detail, ) return html class JQueryTown(forms.TextInput): """ Town fields with state and department pre-selections """ def __init__(self, source, options={}, attrs={}, new=False, limit={}): self.options = None self.attrs = {} self.source = source if len(options) > 0: self.options = JSONEncoder().encode(options) self.attrs.update(attrs) self.new = new self.limit = limit @classmethod def encode_source(cls, source): if isinstance(source, list): encoded_src = JSONEncoder().encode(source) elif isinstance(source, str): src = escape(source) if not src.endswith("/"): src += "/" encoded_src = "'%s'" % src else: try: src = str(source) if not src.endswith("/"): src += "/" encoded_src = "'%s'" % src except: raise ValueError("source type is not valid") return encoded_src def render(self, name, value, attrs=None, renderer=None): attrs_hidden = self.build_attrs(attrs, {"name": name}) attrs_select = self.build_attrs(attrs) attrs_select["placeholder"] = _("Search...") selected = "" selected_state = "" selected_department = "" if value: hiddens = [] selects = [] if type(value) not in (list, tuple): values = str(escape(smart_text(value))) values = values.replace("[", "").replace("]", "") values = values.split(",") else: values = [] for v in value: values += v.split(",") for v in values: if not v: continue hiddens.append(v) selects.append(v) try: item = models.Town.objects.get(pk=v) selects[-1] = str(item) if item.departement: selected_department = item.departement.number if item.departement.state: selected_state = item.departement.state.number selected = item.pk except (models.Town.DoesNotExist, ValueError): selects.pop() hiddens.pop() if hiddens and selects: attrs_hidden["value"] = hiddens[0] attrs_select["value"] = selects[0] 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" source = self.encode_source(self.source) dct = { "source": mark_safe(source), "selected": selected, "safe_field_id": slugify(name).replace("-", "_"), "field_id": name, } if self.options: dct["options"] = mark_safe("%s" % self.options) dct.update( { "attrs_select": mark_safe(flatatt(attrs_select)), "attrs_hidden": mark_safe(flatatt(attrs_hidden)), "name": name, "states": models.State.objects.all().order_by("label"), "selected_department": selected_department, "selected_state": selected_state, } ) html = loader.get_template("blocks/JQueryAdvancedTown.html").render(dct) return html class JQueryPersonOrganization(forms.TextInput): """ Complex widget which manage: * link between person and organization * display addresses of the person and of the organization * create new person and new organization """ def __init__( self, source, edit_source, model, options={}, attrs={}, new=False, limit={}, html_template="blocks/PersonOrganization.html", js_template="blocks/JQueryPersonOrganization.js", ): self.options = None self.attrs = {} self.model = model self.source = source self.edit_source = edit_source if len(options) > 0: self.options = JSONEncoder().encode(options) self.attrs.update(attrs) self.new = new self.limit = limit self.js_template = js_template self.html_template = html_template @classmethod def encode_source(cls, source): if isinstance(source, list): encoded_src = JSONEncoder().encode(source) elif isinstance(source, str) or isinstance(source, str): encoded_src = "'%s'" % escape(source) else: try: encoded_src = "'" + str(source) + "'" except: raise ValueError("source type is not valid") return encoded_src def render_js(self, field_id, selected=""): source = self.encode_source(self.source) edit_source = self.encode_source(self.edit_source) dct = { "source": mark_safe(source), "edit_source": mark_safe(edit_source), "selected": selected, "safe_field_id": slugify(field_id).replace("-", "_"), "field_id": field_id, } if self.options: dct["options"] = mark_safe("%s" % self.options) js = loader.get_template(self.js_template).render(dct) return js def render(self, name, value, attrs=None, renderer=None): attrs_hidden = self.build_attrs(attrs, {"name": name}) attrs_select = self.build_attrs(attrs) attrs_select["placeholder"] = _("Search...") selected = "" if value: hiddens = [] selects = [] if type(value) not in (list, tuple): values = str(escape(smart_text(value))) values = values.replace("[", "").replace("]", "") values = values.split(",") else: values = [] for v in value: values += v.split(",") for v in values: if not v: continue hiddens.append(v) selects.append(v) if self.model: try: item = self.model.objects.get(pk=v) selects[-1] = str(item) selected = item.pk except (self.model.DoesNotExist, ValueError): selects.pop() hiddens.pop() if hiddens and selects: attrs_hidden["value"] = hiddens[0] attrs_select["value"] = selects[0] 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" new = "" dct = { "attrs_select": mark_safe(flatatt(attrs_select)), "attrs_hidden": mark_safe(flatatt(attrs_hidden)), "name": name, "js": self.render_js(name, selected), "new": mark_safe(new), } html = loader.get_template(self.html_template).render(dct) return html class DataTable(Select2Media, forms.RadioSelect): def __init__( self, source, form, associated_model, attrs=None, table_cols="TABLE_COLS", multiple=False, multiple_cols=None, new=False, new_message="", source_full=None, multiple_select=False, sortname="__default__", col_prefix="", gallery=False, map=False, ): """ DataTable widget init. :param source: url to get the item from -- get_item :param form: :param associated_model: model of the listed items :param attrs: :param table_cols: :param multiple: :param multiple_cols: :param new: :param new_message: :param source_full: url to get full listing :param multiple_select: select multiple is available :param sortname: column name (model attribute) to use to sort :param col_prefix: prefix to remove to col_names :param gallery: display the gallery if True :param map: display the map if True - can be a callable """ super(DataTable, self).__init__(attrs=attrs) self.source = source self.form = form if not attrs: attrs = {} self.attrs = attrs.copy() self.associated_model = associated_model self.table_cols = table_cols self.multiple = multiple self.multiple_select = multiple_select if not multiple_cols: multiple_cols = [2] self.multiple_cols = multiple_cols self.new, self.new_message = new, new_message self.source_full = source_full self.sortname = sortname self.col_prefix = col_prefix self.user = None self.gallery = gallery self.map = map self.external_sources = None if self.col_prefix and not self.col_prefix.endswith("__"): self.col_prefix += "__" def get_cols(self, python=False): jq_col_names, extra_cols = [], [] table_cols, col_labels = get_columns_from_class( self.associated_model, table_cols_attr=self.table_cols ) for col_names in table_cols: field_verbose_names = [] field_verbose_name, field_name = "", "" if type(col_names) not in (list, tuple): col_names = [col_names] for col_name in col_names: field = self.associated_model keys = col_name.split("__") if "." in col_name: keys = col_name.split(".") for key in keys: if hasattr(field, "remote_field") and field.remote_field: field = field.remote_field.model try: field = field._meta.get_field(key) field_verbose_name = field.verbose_name except (fields.FieldDoesNotExist, AttributeError): if hasattr(field, key + "_lbl"): field_verbose_name = getattr(field, key + "_lbl") else: continue if field_name: field_name += "__" if col_name.startswith(self.col_prefix): field_name += col_name[len(self.col_prefix) :] else: field_name += col_name field_verbose_names.append(str(field_verbose_name)) if not field_name: field_name = "__".join(col_names) if field_name in col_labels: lbl = col_labels[field_name] if callable(lbl): lbl = lbl() jq_col_names.append(str(lbl)) elif col_names and col_names[0] in col_labels: jq_col_names.append(str(col_labels[col_names[0]])) else: jq_col_names.append( settings.JOINT.join([f for f in field_verbose_names if f]) ) extra_cols.append(field_name) return jq_col_names, extra_cols def render(self, name, value, attrs=None, renderer=None): # t = loader.get_template('blocks/form_flex_snippet.html') t = loader.get_template("blocks/bs_form_snippet.html") if self.user: form = self.form(user=self.user) if self.user.ishtaruser and self.user.ishtaruser.show_field_number(): form.show_field_number = True else: form = self.form() rendered = t.render({"form": form, "search": True}) dct = {} if self.new: model_name = self.associated_model._meta.object_name.lower() dct["url_new"] = reverse("new-" + model_name, args=["0"]) dct["new_message"] = self.new_message col_names, extra_cols = self.get_cols() col_idx = [] for k in form.get_input_ids(): col_idx.append('"%s"' % k) col_idx = col_idx and ", ".join(col_idx) or "" dct["encoding"] = settings.ENCODING or "utf-8" try: dct["source"] = str(self.source) except NoReverseMatch: logger.warning("Cannot resolve source for {} widget".format(self.form)) # full CSV export currently disabled # if str(self.source_full) and str(self.source_full) != 'None': # dct['source_full'] = str(self.source_full) dct["extra_sources"] = [] dct["quick_actions"] = [] if self.associated_model: dct["current_model"] = self.associated_model model_name = "{}.{}".format( self.associated_model.__module__, self.associated_model.__name__ ) for imp in models.ImporterType.objects.filter( slug__isnull=False, associated_models__klass=model_name, is_template=True, ).all(): dct["extra_sources"].append( (imp.slug, imp.name, reverse("get-by-importer", args=[imp.slug])) ) if hasattr(self.associated_model, "QUICK_ACTIONS"): dct["quick_actions"] = self.associated_model.get_quick_actions( user=self.user ) source = str(self.source) dct.update( { "name": name, "col_names": col_names, "extra_cols": extra_cols, "source": source, "col_idx": col_idx, "no_result": str(_("No results")), "loading": str(_("Loading...")), "remove": str(_("Remove")), "sname": name.replace("-", ""), "external_sources": self.external_sources, "gallery": self.gallery, "use_map": self.map() if callable(self.map) else self.map, "multiple": self.multiple, "multiple_select": self.multiple_select, "multi_cols": ",".join(('"%d"' % col for col in self.multiple_cols)), } ) t = loader.get_template("blocks/DataTables.html") rendered += t.render(dct) return mark_safe(rendered) class RangeInput(NumberInput): input_type = "range" class ReversedOSMWidget(gis_forms.OSMWidget): def get_context(self, name, value, attrs): if value: if not isinstance(value, str): # should be geo value = reverse_coordinates(value.ewkt) context = super().get_context(name, value, attrs) return context