#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2016 É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. from django import forms from django.conf import settings from django.core.urlresolvers import reverse from django.db.models import fields from django.forms import ClearableFileInput from django.forms.widgets import flatatt, \ CheckboxSelectMultiple as CheckboxSelectMultipleBase from django.template import Context, loader from django.template.defaultfilters import slugify from django.utils.encoding import smart_unicode from django.utils.functional import lazy from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.simplejson import JSONEncoder from django.utils.translation import ugettext_lazy as _ from ishtar_common import models reverse_lazy = lazy(reverse, unicode) class Select2Multiple(forms.SelectMultiple): class Media: css = { 'all': ('select2/css/select2.css',) } js = ('select2/js/select2.min.js', 'select2/js/init.js') def render(self, name, value, attrs=None, choices=()): klass = attrs and attrs.get('class') or '' klass += ' ' if klass else '' + 'js-select2' if not attrs: attrs = {} attrs['class'] = klass return super(Select2Multiple, self).render(name, value, attrs, choices) 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, unicode): value = value.split(',') if type(value) not in (list, tuple): value = [value] return super(CheckboxSelectMultiple, self).render(name, value, attrs, choices) class MultipleAutocompleteField(forms.MultipleChoiceField): def __init__(self, *args, **kwargs): self.model = None if 'model' in kwargs: self.model = kwargs.pop('model') if 'choices' not in kwargs and self.model: kwargs['choices'] = [] new = kwargs.pop('new') if 'new' in kwargs else None if 'widget' not in kwargs and self.model: kwargs['widget'] = JQueryAutoComplete( reverse_lazy('autocomplete-' + self.model.__name__.lower()), associated_model=self.model, new=new, multiple=True) super(MultipleAutocompleteField, self).__init__(*args, **kwargs) def get_choices(self): return [(i.pk, unicode(i)) for i in self.model.objects.all()] def valid_value(self, value): if not self.model: return super(MultipleAutocompleteField, self).valid_value(value) return bool(self.model.objects.filter(pk=value).count()) def clean(self, value): if value: # clean JS messup with values try: if type(value) not in (list, tuple): value = [int(value)] else: val = value value = [] for v in val: v = unicode(v).strip('[').strip(']')\ .strip('u').strip("'").strip('"') value += [int(va.strip()) for va in list(set(v.split(','))) if va.strip()] except (TypeError, ValueError): value = [] else: value = [] return super(MultipleAutocompleteField, self).clean(value) class DeleteWidget(forms.CheckboxInput): def render(self, name, value, attrs=None): final_attrs = flatatt(self.build_attrs(attrs, name=name, value='1')) output = [''] output.append(u"%s" % (final_attrs, _("Delete"))) output.append('') return mark_safe('\n'.join(output)) class ImageFileInput(ClearableFileInput): template_with_initial = u'%(initial)s'\ u' %(clear_template)s
%(input_text)s: %(input)s' class SquareMeterWidget(forms.TextInput): def render(self, name, value, attrs=None): if not value: value = u"" final_attrs = flatatt(self.build_attrs(attrs, name=name, value=value)) dct = {'final_attrs': final_attrs, 'unit': settings.SURFACE_UNIT_LABEL, 'id': attrs['id'], "safe_id": attrs['id'].replace('-', '_')} t = loader.get_template('blocks/SquareMeterWidget.html') rendered = t.render(Context(dct)) return mark_safe(rendered) AreaWidget = forms.TextInput if settings.SURFACE_UNIT == 'square-metre': AreaWidget = SquareMeterWidget class JQueryDate(forms.TextInput): def __init__(self, *args, **kwargs): super(JQueryDate, self).__init__(*args, **kwargs) if 'class' not in self.attrs: self.attrs['class'] = '' self.attrs['class'] = 'date-pickup' def render(self, name, value=None, attrs=None): if value: value = unicode(value) # very specific... if settings.COUNTRY == 'fr' and value and '/' in value: values = value.split('/') if len(values) == 3: value = "%s-%s-%s" % (values[2], values[1], values[0]) if not attrs: attrs = {} attrs['autocomplete'] = 'off' rendered = super(JQueryDate, self).render(name, value, attrs) # use window.onload to be sure that datepicker don't interfere # with autocomplete fields var_name = name.replace('-', '_') rendered += """ """ % {"name": name, "var_name": var_name, "country": settings.COUNTRY} return rendered class JQueryAutoComplete(forms.TextInput): def __init__(self, source, associated_model=None, options={}, attrs={}, new=False, url_new='', multiple=False, limit={}, dynamic_limit=[]): """ 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.source = source self.associated_model = associated_model if len(options) > 0: self.options = JSONEncoder().encode(options) self.attrs.update(attrs) self.new = new self.url_new = url_new self.multiple = multiple self.limit = limit self.dynamic_limit = dynamic_limit def value_from_datadict(self, data, files, name): if self.multiple: return data.getlist(name, None) else: return data.get(name, None) def render_js(self, field_id): if isinstance(self.source, list): source = JSONEncoder().encode(self.source) elif isinstance(self.source, str) or isinstance(self.source, unicode): source = "'%s'" % escape(self.source) else: try: source = "'" + unicode(self.source) + "'" except: raise ValueError('source type is not valid') dynamic_limit = [ 'id_' + lim.replace('_', '') + '-' + '-'.join(field_id.split('-')[1:-1]) + '-' + lim for lim in self.dynamic_limit ] dct = {'source': mark_safe(source), 'field_id': field_id, 'dynamic_limit': dynamic_limit} if self.options: dct['options'] = mark_safe('%s' % self.options) js = "" tpl = 'blocks/JQueryAutocomplete.js' if self.multiple: tpl = 'blocks/JQueryAutocompleteMultiple.js' t = loader.get_template(tpl) js = t.render(Context(dct)) return js def render(self, name, value=None, attrs=None): attrs_hidden = self.build_attrs(attrs, name=name) attrs_select = self.build_attrs(attrs) attrs_select['placeholder'] = _(u"Search...") if value: hiddens = [] selects = [] values = value if type(value) not in (list, tuple): values = unicode(escape(smart_unicode(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] = unicode( 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' new = '' if self.new: model_name = self.associated_model._meta.object_name.lower() limits = [] for k in self.limit: limits.append(k + "__" + "-".join( [unicode(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 = u' +' % url_new html = u'''%(new)s\ \ ''' % { 'attrs_select': flatatt(attrs_select), 'attrs_hidden': flatatt(attrs_hidden), 'js': self.render_js(name), 'new': new } return html class JQueryTown(forms.TextInput): """ Town fields whith 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) \ or isinstance(source, unicode): src = escape(source) if not src.endswith('/'): src += "/" encoded_src = "'%s'" % src else: try: src = unicode(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=None, attrs=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 = unicode(escape(smart_unicode(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] = unicode(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(Context(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, unicode): encoded_src = "'%s'" % escape(source) else: try: encoded_src = "'" + unicode(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(Context(dct)) return js def render(self, name, value=None, attrs=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 = unicode(escape(smart_unicode(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] = unicode(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(Context(dct)) return html class JQueryJqGrid(forms.RadioSelect): COL_TPL = "{name:'%(idx)s', index:'%(idx)s', sortable:true}" # class Media: # js = ['%s/js/i18n/grid.locale-%s.js' % (settings.STATIC_URL, # settings.COUNTRY), # '%s/js/jquery.jqGrid.min.js' % settings.STATIC_URL] # css = {'all': ['%s/media/ui.jqgrid.css' % settings.STATIC_URL]} def __init__(self, source, form, associated_model, attrs={}, table_cols='TABLE_COLS', multiple=False, multiple_cols=[2], new=False, new_message="", source_full=None, multiple_select=False, sortname="__default__", col_prefix=''): """ JQueryJqGrid 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: :param sortname: column name (model attribute) to use to sort :param col_prefix: prefix to remove to col_names """ super(JQueryJqGrid, 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 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 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 = {} if hasattr(self.associated_model, self.table_cols + '_LBL'): col_labels = getattr(self.associated_model, self.table_cols + '_LBL') for col_names in getattr(self.associated_model, self.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(unicode(field_verbose_name)) if not field_name: field_name = "__".join(col_names) if field_name in col_labels: jq_col_names.append(unicode(col_labels[field_name])) elif col_names and col_names[0] in col_labels: jq_col_names.append(unicode(col_labels[col_names[0]])) else: jq_col_names.append(settings.JOINT.join( [f for f in field_verbose_names if f])) if not python: jq_col_names[-1] = u'"%s"' % jq_col_names[-1] if python: extra_cols.append(field_name) else: extra_cols.append(self.COL_TPL % {'idx': field_name}) if not python: jq_col_names = jq_col_names and ", ".join(jq_col_names) or "" extra_cols = extra_cols and ", ".join(extra_cols) or "" return jq_col_names, extra_cols def render(self, name, value=None, attrs=None): t = loader.get_template('blocks/form_flex_snippet.html') form = self.form() rendered = t.render(Context({'form': form, 'flex': 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' dct['source'] = unicode(self.source) if unicode(self.source_full) and unicode(self.source_full) != 'None': dct['source_full'] = unicode(self.source_full) dct['extra_sources'] = [] if 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=model_name, is_template=True).all(): dct['extra_sources'].append(( imp.slug, imp.name, reverse('get-by-importer', args=[imp.slug]))) dct.update({'name': name, 'col_names': col_names, 'extra_cols': extra_cols, 'source': unicode(self.source), 'col_idx': col_idx, 'no_result': unicode(_("No results")), 'loading': unicode(_("Loading...")), 'remove': unicode(_(u"Remove")), 'sname': name.replace('-', ''), '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/JQueryJqGrid.html') rendered += t.render(Context(dct)) return mark_safe(rendered)