SHOW:
|
|
- or go back to the newest paste.
1 | # -*- coding: utf-8 -*- | |
2 | ||
3 | """ | |
4 | - | from django.utils.encoding import force_unicode |
4 | + | Notes: |
5 | - | from django.core.urlresolvers import reverse |
5 | + | - The javascript will only allow values provided by the callback. If nothing is selected, the text will revert back |
6 | - | from django.forms.util import ErrorList, ValidationError |
6 | + | to the previously selected value (or blank, if nothing was set). Also no ID will be set. |
7 | - | from django.utils.translation import ugettext as _ |
7 | + | |
8 | -- TODO -- | |
9 | - use a user-specified label-method to generate the label. Currently this is done in two places, which is not that nice | |
10 | - Write Tests | |
11 | - | <input type="text" class="ac_input" name="%(name)s_text" id="%(html_id)s_text" value="%(text)s"/> |
11 | + | - CSRF-Exempt neccessary? |
12 | - | <input type="hidden" name="%(name)s" id="%(html_id)s" value="%(value)s" /> |
12 | + | - |
13 | ||
14 | - | $(function(){ |
14 | + | >>> # The Models |
15 | - | $("#%(html_id)s_text").autocomplete({ |
15 | + | >>> class ModelB(models.Model): |
16 | - | 'source': '%(url)s', |
16 | + | >>> value = models.CharField(max_length=255) |
17 | - | 'minLength': 2, |
17 | + | >>> |
18 | - | 'select': function(event, ui) { |
18 | + | >>> def __repr__(self): |
19 | - | $("#%(html_id)s") |
19 | + | >>> return self.value |
20 | >>> | |
21 | - | .data('prev', ui.item.label) |
21 | + | >>> |
22 | - | .trigger('change'); |
22 | + | >>> class ModelA(models.Model): |
23 | - | %(callback)s |
23 | + | >>> #... some fields ... |
24 | - | }, |
24 | + | >>> model_b = models.ForeignKey(ModelB) |
25 | - | 'close': function(event, ui) { |
25 | + | >>> |
26 | - | // dont restore the old value here. It should be better to restore it in the blur-function */ |
26 | + | >>> |
27 | - | /*setTimeout(function() { |
27 | + | >>> # The Form |
28 | - | var prev = $('#%(html_id)s').data('prev'); |
28 | + | >>> class MyForm(ModelForm): |
29 | - | $("#%(html_id)s_text").val(prev); |
29 | + | >>> class Meta: |
30 | - | }, 100);*/ |
30 | + | >>> model = ModelA |
31 | - | } |
31 | + | >>> |
32 | - | }) |
32 | + | >>> # Some Fields |
33 | - | .blur(function() { |
33 | + | >>> model_b = ModelAutocompleteField(ModelB, url=reverse("myproject.myapp.views.callback")) |
34 | - | var prev = $('#%(html_id)s').data('prev'); |
34 | + | >>> |
35 | - | $("#%(html_id)s_text").val(prev); |
35 | + | >>> |
36 | - | }) |
36 | + | >>> # The callback in the view |
37 | - | .data("autocomplete")._renderItem = function( ul, item ) { |
37 | + | >>> def callback(request): |
38 | >>> search_term = request.GET['term'] | |
39 | >>> matches = ModelB.objects.filter(value__contains = search_term).all() | |
40 | >>> return_values = [{"id": match.pk, "label": match.value} for match in matches] | |
41 | >>> return HttpResponse(simplejson.dumps(return_values)) | |
42 | - | }; |
42 | + | >>> |
43 | """ | |
44 | - | // Speichern des initialien Werts |
44 | + | |
45 | - | $("#%(html_id)s").data('prev', '%(value)s'); |
45 | + | |
46 | - | }); |
46 | + | from django.forms.util import ValidationError |
47 | from django.utils.safestring import mark_safe | |
48 | ||
49 | ||
50 | - | class ModelAutoCompleteWidget(forms.widgets.TextInput): |
50 | + | |
51 | CLIENT_CODE = u""" | |
52 | - | Autocomplete-Widget. Rendert die Inputs und das JAvascript um das AutoComplete-Field |
52 | + | <input type="text" class="ac_input" name="{name}_label" id="{id}_label" value="{label}"/> |
53 | - | einzubinden. Kann nur in Verbindung mit dem ModelAutoCompleteField benutzt werden. |
53 | + | <input type="hidden" name="{name}" id="{id}" value="{value}" /> |
54 | - | (Fuer den Text-wert wird das Model benoetigt, es wird also vom Field an dieses Widget |
54 | + | |
55 | - | weitergegeben) |
55 | + | $(function(){{ |
56 | ||
57 | - | def __init__(self, url, model, callback=None, *args, **kw): |
57 | + | // Store the initial value into the hidden field |
58 | - | super(forms.widgets.TextInput, self).__init__(*args, **kw) |
58 | + | $("#{id}").data('previous_label', '{label}'); |
59 | - | self.url = url |
59 | + | console.log($("#{id}_label")); |
60 | - | self.model = model |
60 | + | // attach the jquery-autocomplete to the label-input |
61 | - | self.callback = callback |
61 | + | $("#{id}_label").autocomplete({{ |
62 | 'source': '{url}', | |
63 | - | |
63 | + | 'minLength': 2, // length of inputs neccessary to trigger the autocomplete |
64 | 'select': function(event, ui) {{ | |
65 | // if a new value is selected from the autocomplete update the id-field and notify possible observer | |
66 | $("#{id}") | |
67 | - | Rendert das Widget |
67 | + | |
68 | .data('previous_label', ui.item.label) | |
69 | - | Der Name des Widgets |
69 | + | .trigger('change') |
70 | .trigger('autocomplete:change', this, ui.item.id); | |
71 | - | Der Wert den der Hauptinput (In diesem Fall ID) haben soll |
71 | + | |
72 | }}, | |
73 | - | # Defaults falls was nicht Ok ist: |
73 | + | 'close': function(event, ui) {{ |
74 | - | text = '' |
74 | + | // dont restore the old value here. It should be better to restore it in the blur-function |
75 | - | val = '' |
75 | + | }} |
76 | }}) | |
77 | - | # Wenn Wert gegeben wurde versuchen die Daten zu holen |
77 | + | .blur(function() {{ |
78 | // if we blur (loose focus) restore the old value. | |
79 | - | try: |
79 | + | // if we already selected a value, this will be the new value, otherwise it will be the old |
80 | - | text = self.model.objects.get(pk=value) |
80 | + | var previous_label = $('#{id}').data('previous_label'); |
81 | - | val = value |
81 | + | $("#{id}_label").val(previous_label); |
82 | - | except Exception: |
82 | + | }}) |
83 | - | pass |
83 | + | .data("ui-autocomplete")._renderItem = function( ul, item ) {{ |
84 | // Use a custom render function, so we use item.label instead of just item | |
85 | return $( "<li></li>" ) | |
86 | - | html_id = attrs.get('id', name) |
86 | + | |
87 | - | url = self.url() if callable(self.url) else self.url |
87 | + | |
88 | - | callback = u"%s(ui.item.id);" % self.callback if self.callback else "" |
88 | + | |
89 | }} | |
90 | - | return mark_safe(CLIENT_CODE % dict(name=name, html_id=html_id, url=url, value=val, text=text, callback=callback)) |
90 | + | }}); |
91 | </script> | |
92 | """ | |
93 | - | def value_from_datadict(self, data, files, name): |
93 | + | |
94 | class ModelAutocompleteWidget(forms.widgets.TextInput): | |
95 | """ | |
96 | The autocomplete-widget for the autocomplete-field. This will render the above CLIENT_CODE | |
97 | containing the two inputs and the neccessary javascript. | |
98 | ||
99 | - | return data.get(name, None) |
99 | + | To display the text this widget required the model, so it gets a reference. For the label-value, _repr_method is called with |
100 | an instance of the model. Currently this defaults to repr | |
101 | ||
102 | The widget is based on a TextInput-Field | |
103 | """ | |
104 | - | class ModelAutoCompleteField(forms.fields.CharField): |
104 | + | def __init__(self, url, model, *args, **kw): |
105 | """ | |
106 | - | Autocomplete-Feld das die gelesenen Werte direkt zurueck in Django-Model-Objekte |
106 | + | Initing the Widget |
107 | - | konvertiert. Als Widget wird das AutoCOmpleteWidget von oben benutzt, was die |
107 | + | `url` |
108 | - | JavaScript-Logik und Felder korrekt fuellt. |
108 | + | The url used as callback. Must return a JSON-String in the format of [{"id": 1, "label": "..."}, {"id": 2, "label": "..."}, ...] |
109 | - | Momentan muss ein gueltiger WErt selektiert werden. |
109 | + | |
110 | The model the Autocomplete is based on | |
111 | """ | |
112 | super(ModelAutocompleteWidget, self).__init__(*args, **kw) | |
113 | - | 'invalid_choice': mark_safe(u'Keine gültige Auswahl. Bitte eine gültige Option wählen.'), |
113 | + | self._url = url |
114 | - | 'no_choice' : mark_safe(u'Kein Eintrag ausgewählt. Bitte wählen sie einen Eintrag.'), |
114 | + | self._model = model |
115 | self._repr_method = repr | |
116 | ||
117 | def render(self, name, value, attrs=None): | |
118 | - | def __init__(self, model, url, callback=None,size=None, *args, **kwargs): |
118 | + | |
119 | Rendering the HTML-Code of the Widget | |
120 | - | Neues AutoCOmpleteField anlegen |
120 | + | |
121 | The name of the widget. Used to specify the id- and name-tags of the input-field | |
122 | `value` | |
123 | The value is the id of the entity to display (=the id). It is used to load the label from the model | |
124 | """ | |
125 | ||
126 | - | self.model = model |
126 | + | # If there is a value try to load the Object |
127 | if value != None: | |
128 | - | # build attrs for widget |
128 | + | try: |
129 | value, label = value, self._repr_method(self._model.objects.get(pk=value)) | |
130 | - | attrs = {'size': size} if size else {} |
130 | + | except self.__model.DoesNotExist: |
131 | value, label = "", "" | |
132 | - | super(ModelAutoCompleteField, self).__init__( |
132 | + | else: |
133 | - | widget = ModelAutoCompleteWidget(url=url, model=model, callback=callback, attrs=attrs), |
133 | + | value, label = "", "" |
134 | - | *args, |
134 | + | |
135 | - | **kwargs) |
135 | + | |
136 | id = attrs.get('id', name) | |
137 | - | def update_url(self, url): |
137 | + | _url = self._url |
138 | - | self.widget.url = url |
138 | + | _url = _url() if callable(_url) else _url |
139 | ||
140 | - | def clean(self, value): |
140 | + | return mark_safe(CLIENT_CODE.format(name=name, id=id, url=_url, value=value, label=label)) |
141 | ||
142 | - | Die Methode konvertiert die ID des HiddenFields zurueck in ein Django-MOdel |
142 | + | # Not neccessary? |
143 | #def value_from_datadict(self, data, files, name): | |
144 | """ | |
145 | - | # Kein WErt gegeben, es wird aber auch keiner gebraucht |
145 | + | |
146 | - | if value=='' and not self.required: |
146 | + | |
147 | """ | |
148 | # return data.get(name, None) | |
149 | - | # Wenn kein Wert gegeben ist kann auch kein Model gelesen werden |
149 | + | |
150 | - | if value=='': |
150 | + | |
151 | ||
152 | ||
153 | - | # Versuchen das Objekt zu lesen |
153 | + | class ModelAutocompleteField(forms.fields.CharField): |
154 | - | try: |
154 | + | |
155 | - | obj = self.model.objects.get(pk=value) |
155 | + | This field renders the associated modul-column using an hidden id-field and a visible |
156 | - | except self.model.DoesNotExist: |
156 | + | autocomplete textfield. |
157 | - | raise ValidationError(self.error_messages['invalid_choice']) |
157 | + | This inherits from CharField and just extends the init-function, for additional parameter |
158 | and the to_python function, to convert the integer-value (Forein key) back to an instance of the Model | |
159 | - | return obj |
159 | + | |
160 | ||
161 | default_error_messages = { | |
162 | 'invalid_choice': mark_safe(u'Invalid Option. Please choose a valid option!'), | |
163 | 'no_choice' : mark_safe(u'No option selected. Please choose a valid option!'), | |
164 | } | |
165 | ||
166 | def __init__(self, model, url, *args, **kwargs): | |
167 | """ | |
168 | Create the autocomplete-field. Calls the parent-init-method and replaces the default-widget | |
169 | for a CharField with a ModelAutocompleteWidget | |
170 | `model` | |
171 | Das Model auf dem die Vervollstaendigung beruht. | |
172 | `url` | |
173 | Die URL aus der die Callbackdatan angefragt werden | |
174 | """ | |
175 | self._model = model | |
176 | widget = ModelAutocompleteWidget(url=url, model=model) | |
177 | super(ModelAutocompleteField, self).__init__(widget=widget, *args, **kwargs) | |
178 | ||
179 | # Not neccessary anymore? | |
180 | #def update_url(self, url): | |
181 | #self.widget.url = url | |
182 | ||
183 | def to_python(self, value): | |
184 | """ | |
185 | Converts the given value (PK of the model) to an instance of the model itself. | |
186 | If the id is not valid or empty, an appropriate ValidationError is thrown | |
187 | `value` | |
188 | The value from the POST | |
189 | """ | |
190 | # if no value is given, but we arent required just return None | |
191 | if value == "" and not self.required: | |
192 | return None | |
193 | ||
194 | # If no value is given, but we need one | |
195 | elif value == "" and self.required: | |
196 | raise ValidationError(self.error_messages['no_choice']) | |
197 | ||
198 | # Else we have a value, try to load the object-instance and return it | |
199 | else: | |
200 | try: | |
201 | return self._model.objects.get(pk=value) | |
202 | except self._model.DoesNotExist: | |
203 | raise ValidationError(self.error_messages['invalid_choice']) |