#!/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.core.exceptions import ValidationError from django.core.files import File from django.core.urlresolvers import reverse, NoReverseMatch 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.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 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=()): if value: self.choices = list(self.get_choices(value)) value = self.choices[0][0] return super(SelectReadonly, self).render(name, value, attrs) 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, required, widget, label, initial, 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(object): @property def media(self): media = super(Select2Media, self).media 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.add_css(css) media.add_js(js) return media class Select2Dynamic(Select2Media, forms.Select): """ Select input using select, allowing dynamic creation. """ def render(self, name, value, attrs=None, choices=()): choices = choices or getattr(self, 'choices', []) if value and value not in [key for key, v in choices]: choices.insert(1, (value, value)) 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 = [ u"tags: true", ] ''' msg = str( _(u"Are you sure you want to add this term? (the addition is " u"effective after registration of the element)") ) options.append(u"""createTag: function (params) {{ return confirm("{}"); }}""".format(msg)) ''' if attrs.get('full-width', None): options.append(u"containerCssClass: 'full-width'") html = super(Select2Dynamic, self).render(name, value, attrs) html += """ """.format(name, u", ".join(options)) return mark_safe(html) 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 u'"' in value: raise ValidationError( _(u"The character \" is not accepted.") ) def to_python(self, value): """ Strip value """ return super(Select2DynamicField, self).to_python(value).strip() 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=()): 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 = u"
" 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) 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=()): 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) 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 = u"%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 = u""" {} """.format(extra_class, final_attrs, extra_label) return mark_safe(output) class DeleteSwitchWidget(SwitchWidget): extra_class = "danger" extra_label = _(u"Delete") 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 = u"" 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/SquareMeterWidget.html' EXTRA_DCT = {'unit': settings.SURFACE_UNIT_LABEL} class GramKilogramWidget(CustomWidget): TEMPLATE = 'widgets/GramKilogramWidget.html' EXTRA_DCT = {'unit': "g"} class CentimeterMeterWidget(CustomWidget): TEMPLATE = 'widgets/CentimeterMeterWidget.html' EXTRA_DCT = {'unit': "cm"} 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( _(u"{} 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'] = _(u"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 += u"".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'] = _(u"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'] = _(u"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 if self.col_prefix and not self.col_prefix.endswith('__'): self.col_prefix += "__" def get_cols(self, python=False): jq_col_names, extra_cols = [], [] col_labels = {} slug = getattr(self.associated_model, 'SLUG', None) if hasattr(self.associated_model, 'COL_LABELS'): col_labels = self.associated_model.COL_LABELS if slug in settings.TABLE_COLS: col_labels.update(settings.TABLE_COLS[slug]) tb_key = (slug, self.table_cols) if tb_key in settings.TABLE_COLS: table_cols = settings.TABLE_COLS[tb_key] else: table_cols = getattr(self.associated_model, self.table_cols) if callable(table_cols): table_cols = 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, 'rel') and field.rel: field = field.rel.to 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(u'"%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(_(u"Remove")), 'sname': name.replace('-', ''), '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((u'"%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'