From 39e296ed421cce7696313dcafe75cd03d073054d Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Thu, 26 Dec 2013 19:09:09 +0100 Subject: Manage archaeological sites into forms (refs #1586) * create new widget: multiple autocomplete field * move JS autocomplete to template * archaeological site reference made unique --- .../templates/blocks/JQueryAutocomplete.js | 19 ++++ .../templates/blocks/JQueryAutocompleteMultiple.js | 92 +++++++++++++++++ ishtar_common/templatetags/replace_underscore.py | 10 ++ ishtar_common/widgets.py | 113 ++++++++++++++++----- ishtar_common/wizards.py | 30 +++++- 5 files changed, 235 insertions(+), 29 deletions(-) create mode 100644 ishtar_common/templates/blocks/JQueryAutocomplete.js create mode 100644 ishtar_common/templates/blocks/JQueryAutocompleteMultiple.js create mode 100644 ishtar_common/templatetags/replace_underscore.py (limited to 'ishtar_common') diff --git a/ishtar_common/templates/blocks/JQueryAutocomplete.js b/ishtar_common/templates/blocks/JQueryAutocomplete.js new file mode 100644 index 000000000..eb365c38a --- /dev/null +++ b/ishtar_common/templates/blocks/JQueryAutocomplete.js @@ -0,0 +1,19 @@ +$("#id_select_{{field_id}}").autocomplete({ + source: {{source}}, + select: function( event, ui ) { + if(ui.item){ + $('#id_{{field_id}}').val(ui.item.id); + } else { + $('#id_{{field_id}}').val(null); + } + }, + minLength: 2{% if options %}, + {{options}} + {% endif %} +}); + +$('#id_select_{{field_id}}').live('click', function(){ + $('#id_{{field_id}}').val(null); + $('#id_select_{{field_id}}').val(null); +}); + diff --git a/ishtar_common/templates/blocks/JQueryAutocompleteMultiple.js b/ishtar_common/templates/blocks/JQueryAutocompleteMultiple.js new file mode 100644 index 000000000..56133ef4e --- /dev/null +++ b/ishtar_common/templates/blocks/JQueryAutocompleteMultiple.js @@ -0,0 +1,92 @@ +{% load replace_underscore %} +function split( val ) { + return val.split( /,\s*/ ); +} + +function extractLast( term ) { + return split( term ).pop(); +} + +var {{field_id|replace_underscore}}_values = $('#id_{{field_id}}').val().split(','); +if(!{{field_id|replace_underscore}}_values){ + {{field_id|replace_underscore}}_values = new Array(); +} +var {{field_id|replace_underscore}}_ctext = ""; + +$("#id_select_{{field_id}}") + // don't navigate away from the field on tab when selecting an item + .bind( "keydown", function( event ) { + if ( event.keyCode === $.ui.keyCode.TAB && + $( this ).data( "ui-autocomplete" ).menu.active ) { + event.preventDefault(); + } else if (event.keyCode === $.ui.keyCode.DELETE || + event.keyCode === $.ui.keyCode.BACKSPACE){ + {{field_id|replace_underscore}}_ctext = + $("#id_select_{{field_id}}").val().split(','); + } + }) + .bind( "keyup", function( event ) { + if (event.keyCode === $.ui.keyCode.DELETE || + event.keyCode === $.ui.keyCode.BACKSPACE){ + var new_val = $("#id_select_{{field_id}}").val().split(','); + var length = {{field_id|replace_underscore}}_ctext.length; + for (idx=0;idx= new_val.length || + {{field_id|replace_underscore}}_ctext[idx] != new_val[idx]){ + if (idx == (length - 1) && length > 1){ + if ({{field_id|replace_underscore}}_ctext[idx].trim() == ""){ + idx = idx - 1; + } else { + return; + } + } + {{field_id|replace_underscore}}_ctext.splice(idx, 1); + // remove value + if (idx < {{field_id|replace_underscore}}_values.length){ + {{field_id|replace_underscore}}_values.splice(idx, 1); + $('#id_{{field_id}}').val({{field_id|replace_underscore}}_values); + } + if ({{field_id|replace_underscore}}_ctext.length > 0 && + {{field_id|replace_underscore}}_ctext[0].length){ + // remove leading space + if ({{field_id|replace_underscore}}_ctext[0][0] == ' '){ + {{field_id|replace_underscore}}_ctext[0] = + {{field_id|replace_underscore}}_ctext[0].trim(); + } + // remove trailing space + var last = {{field_id|replace_underscore}}_ctext.length -1; + {{field_id|replace_underscore}}_ctext[last] = + {{field_id|replace_underscore}}_ctext[last].trim(); + } + this.value = {{field_id|replace_underscore}}_ctext.join(", "); + return + } + } + } + }).autocomplete({ + source: function( request, response ) { + $.getJSON({{source}}, { + term: extractLast( request.term ) + }, response ); + }, + focus: function() { + // prevent value inserted on focus + return false; + }, + select: function( event, ui ) { + var terms = split( this.value ); + // remove the current input + terms.pop(); + // add the selected item + terms.push( ui.item.value ); + {{field_id|replace_underscore}}_values.push(ui.item.id); + // add placeholder to get the comma-and-space at the end + $('#id_{{field_id}}').val({{field_id|replace_underscore}}_values); + terms.push( "" ); + this.value = terms.join( ", " ); + return false; + }, + minLength: 2{% if options %}, + {{options}} + {% endif %} +}); diff --git a/ishtar_common/templatetags/replace_underscore.py b/ishtar_common/templatetags/replace_underscore.py new file mode 100644 index 000000000..66931e6fe --- /dev/null +++ b/ishtar_common/templatetags/replace_underscore.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from django.template import Library + +register = Library() + +@register.filter +def replace_underscore(value): + return value.replace('-', '_') diff --git a/ishtar_common/widgets.py b/ishtar_common/widgets.py index a97cfe70b..fc3ada283 100644 --- a/ishtar_common/widgets.py +++ b/ishtar_common/widgets.py @@ -27,6 +27,7 @@ from django.forms import ClearableFileInput from django.forms.widgets import flatatt from django.template import Context, loader 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 @@ -34,6 +35,44 @@ from django.utils.translation import ugettext_lazy as _ import models +reverse_lazy = lazy(reverse, unicode) + +class MultipleAutocompleteField(forms.MultipleChoiceField): + def __init__(self, *args, **kwargs): + model = None + if 'model' in kwargs: + model = kwargs.pop('model') + if 'choices' not in kwargs and model: + kwargs['choices'] = [(i.pk, unicode(i))for i in model.objects.all()] + new = kwargs.pop('new') if 'new' in kwargs else None + if 'widget' not in kwargs and model: + kwargs['widget'] = JQueryAutoComplete(reverse_lazy( + 'autocomplete-'+model.__name__.lower()), + associated_model=model, new=new, + multiple=True) + super(MultipleAutocompleteField, self).__init__(*args, **kwargs) + + def clean(self, value): + if value: + # clean JS messup with values + try: + if type(value) not in (list, tuple): + value = [value] + else: + val = value + value = [] + for v in val: + v = unicode(v).strip('[').strip(']' + ).strip('u').strip("'").strip('"') + value += [int(v.strip()) + for v in list(set(v.split(','))) + if v.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, @@ -95,7 +134,7 @@ class JQueryDate(forms.TextInput): class JQueryAutoComplete(forms.TextInput): def __init__(self, source, associated_model=None, options={}, attrs={}, - new=False): + new=False, multiple=False): """ Source can be a list containing the autocomplete values or a string containing the url used for the request. @@ -108,6 +147,13 @@ class JQueryAutoComplete(forms.TextInput): self.options = JSONEncoder().encode(options) self.attrs.update(attrs) self.new = new + self.multiple = multiple + + 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): @@ -119,39 +165,54 @@ class JQueryAutoComplete(forms.TextInput): source = "'" + unicode(self.source) + "'" except: raise ValueError('source type is not valid') - options = 'source : ' + source - options += ''', select: function( event, ui ) { - if(ui.item){ - $('#id_%s').val(ui.item.id); - } else { - $('#id_%s').val(null); - } - }, minLength: 2 - ''' % (field_id, field_id) + dct = {'source':mark_safe(source), + 'field_id':field_id} if self.options: - options += ',%s' % self.options + dct['options'] = mark_safe('%s' % self.options) - js = u'$(\'#id_select_%s\').autocomplete({%s});\n' % (field_id, options) - js += u'''$(\'#id_select_%s\').live('click', function(){ - $('#id_%s').val(null); - $('#id_select_%s').val(null); -});''' % (field_id, field_id, field_id) + 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) - if value: - val = escape(smart_unicode(value)) - attrs_hidden['value'] = val - attrs_select['value'] = val - if self.associated_model: - try: - attrs_select['value'] = unicode( - self.associated_model.objects.get(pk=value)) - except: - attrs_select['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: + attrs_hidden['value'] = hiddens[0] + attrs_select['value'] = selects[0] if not self.attrs.has_key('id'): attrs_hidden['id'] = 'id_%s' % name attrs_select['id'] = 'id_select_%s' % name diff --git a/ishtar_common/wizards.py b/ishtar_common/wizards.py index 55c9d0d9d..a237fb327 100644 --- a/ishtar_common/wizards.py +++ b/ishtar_common/wizards.py @@ -24,12 +24,22 @@ from django.contrib.formtools.wizard.views import NamedUrlWizardView from django.core.exceptions import ObjectDoesNotExist from django.core.files.images import ImageFile from django.db.models.fields.files import FileField +from django.db.models.fields.related import ManyToManyField from django.shortcuts import render_to_response from django.template import RequestContext -from django.utils.datastructures import MultiValueDict +from django.utils.datastructures import MultiValueDict as BaseMultiValueDict from django.utils.translation import ugettext_lazy as _ import models +class MultiValueDict(BaseMultiValueDict): + def get(self, *args, **kwargs): + v = super(MultiValueDict, self).getlist(*args, **kwargs) + if len(v) > 1: + v = ",".join(v) + else: + v = super(MultiValueDict, self).get(*args, **kwargs) + return v + class Wizard(NamedUrlWizardView): model = None label = '' @@ -211,7 +221,8 @@ class Wizard(NamedUrlWizardView): if type(value) in (tuple, list): values = value elif "," in unicode(value): - values = unicode(value).split(",") + values = unicode(value + ).strip('[').strip(']').split(",") else: values = [value] rendered_values = [] @@ -344,6 +355,12 @@ class Wizard(NamedUrlWizardView): isinstance(obj.__class__._meta.get_field(k), ImageFile)): if not dct[k]: dct[k] = None + if isinstance(obj.__class__._meta.get_field(k), + ManyToManyField): + if not dct[k]: + dct[k] = [] + elif type(dct[k]) not in (list, tuple): + dct[k] = [dct[k]] setattr(obj, k, dct[k]) try: obj.full_clean() @@ -516,7 +533,8 @@ class Wizard(NamedUrlWizardView): elif hasattr(form, 'extra_form') and hasattr(form.extra_form, 'fields')\ and form.extra_form.fields.keys(): frm = form.extra_form - elif hasattr(form, 'forms') and form.forms and form.forms[0].fields.keys(): + elif hasattr(form, 'forms') and form.forms \ + and form.forms[0].fields.keys(): frm = form.forms[0] if frm: first_field = frm.fields[frm.fields.keyOrder[0]] @@ -675,6 +693,12 @@ class Wizard(NamedUrlWizardView): value = obj break value = getattr(value, field) + if hasattr(value, 'all') and callable(value.all): + if not value.count(): + continue + initial.setlist(base_field, + [unicode(v.pk) for v in value.all()]) + continue if value == obj: continue if hasattr(value, 'pk'): -- cgit v1.2.3