Codebase list django-ajax-selects / 0193952
Imported Upstream version 1.2.3 SVN-Git Migration 8 years ago
46 changed file(s) with 1758 addition(s) and 741 deletion(s). Raw diff Collapse all Expand all
._README.txt less more
Binary diff not shown
._setup.py less more
Binary diff not shown
0 *.pyc
1 *.egg-info/
2 example/AJAXSELECTS/*
0 recursive-include ajax_select *.css *.py *.gif *.html *.txt *.js *.md
1 recursive-include example *.py *.sh *.txt
0
1 Ordered ManyToMany fields without a full Through model
2 ======================================================
3
4 When re-editing a previously saved model that has a ManyToMany field, the order of the recalled ids can be somewhat random.
5 The user sees Arnold, Bosco, Cooly in the interface, saves, comes back later to edit it and he sees Bosco, Cooly, Arnold. So he files a bug report.
6
7 A proper solution would be to use a separate Through model, an order field and the ability to drag the items in the interface to rearrange. But a proper Through model would also introduce extra fields and that would be out of the scope of ajax_selects. Maybe some future version.
8
9 It is possible however to offer an intuitive experience for the user: save them in the order they added them and the next time they edit it they should see them in same order.
10
11 Problem
12 -------
13
14 class Agent(models.Model):
15 name = models.CharField(blank=True, max_length=100)
16
17 class Apartment(models.Model):
18 agents = models.ManyToManyField(Agent)
19
20 When the AutoCompleteSelectMultipleField saves it does so by saving each relationship in the order they were added in the interface.
21
22 # this query does not have a guaranteed order (especially on postgres)
23 # and certainly not the order that we added them
24 apartment.agents.all()
25
26
27 # this retrieves the joined objects in the order of their id (the join table id)
28 # and thus gets them in the order they were added
29 apartment.agents.through.objects.filter(apt=self).select_related('agent').order_by('id')
30
31
32 Temporary Solution
33 ------------------
34
35 class AgentOrderedManyToManyField(models.ManyToManyField):
36 """ regardless of using a through class,
37 only the Manager of the related field is used for fetching the objects for many to many interfaces.
38 with postgres especially this means that the sort order is not determinable other than by the related field's manager.
39
40 this fetches from the join table, then fetches the Agents in the fixed id order
41 the admin ensures that the agents are always saved in the fixed id order that the form was filled out with
42 """
43 def value_from_object(self,object):
44 from company.models import Agent
45 rel = getattr(object, self.attname)
46 qry = {self.related.var_name:object}
47 qs = rel.through.objects.filter(**qry).order_by('id')
48 aids = qs.values_list('agent_id',flat=True)
49 agents = dict( (a.pk,a) for a in Agent.objects.filter(pk__in=aids) )
50 try:
51 return [agents[aid] for aid in aids ]
52 except KeyError:
53 raise Exception("Agent is missing: %s > %s" % (aids,agents))
54
55 class Apartment(models.Model):
56 agents = AgentOrderedManyToManyField()
57
58
59 class AgentLookup(object):
60
61 def get_objects(self,ids):
62 # now that we have a dependable ordering
63 # we know the ids are in the order they were originally added
64 # return models in original ordering
65 ids = [int(id) for id in ids]
66 agents = dict( (a.pk,a) for a in Agent.objects.filter(pk__in=ids) )
67 return [agents[aid] for aid in ids]
68
+0
-26
PKG-INFO less more
0 Metadata-Version: 1.0
1 Name: django-ajax-selects
2 Version: 1.1.4
3 Summary: jQuery-powered auto-complete fields for ForeignKey and ManyToMany fields
4 Home-page: http://code.google.com/p/django-ajax-selects/
5 Author: crucialfelix
6 Author-email: crucialfelix@gmail.com
7 License: UNKNOWN
8 Description: Enables editing of `ForeignKey`, `ManyToMany` and simple text fields using the Autocomplete - `jQuery` plugin.
9
10 django-ajax-selects will work in any normal form as well as in the admin.
11
12 The user is presented with a text field. They type a search term or a few letters of a name they are looking for, an ajax request is sent to the server, a search channel returns possible results. Results are displayed as a drop down menu. When an item is selected it is added to a display area just below the text field.
13
14
15 Platform: UNKNOWN
16 Classifier: Programming Language :: Python
17 Classifier: Programming Language :: Python :: 2
18 Classifier: Development Status :: 4 - Beta
19 Classifier: Environment :: Web Environment
20 Classifier: Intended Audience :: Developers
21 Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
22 Classifier: Operating System :: OS Independent
23 Classifier: Topic :: Software Development :: Libraries :: Python Modules
24 Classifier: Topic :: Software Development :: User Interfaces
25 Classifier: Framework :: Django
0
1 Enables editing of `ForeignKey`, `ManyToMany` and `CharField` using jQuery UI Autocomplete.
2
3 User experience
4 ===============
5
6 selecting:
7
8 <img src='http://media.crucial-systems.com/posts/selecting.png'/>
9
10 selected:
11
12 <img src='http://media.crucial-systems.com/posts/selected.png'/>
13
14 [Note: screen shots are from the older version. Styling has changed slightly]
15
16 1. The user types a search term into the text field
17 2. An ajax request is sent to the server.
18 3. The dropdown menu is populated with results.
19 4. User selects by clicking or using arrow keys
20 5. Selected result displays in the "deck" area directly below the input field.
21 6. User can click trashcan icon to remove a selected item
22
23 Features
24 ========
25
26 + Django 1.2+
27 + Optional boostrap mode allows easy installation by automatic inclusion of jQueryUI from the googleapis CDN
28 + Compatible with staticfiles, appmedia, django-compressor etc
29 + Popup to add a new item is supported
30 + Admin inlines now supported
31 + Ajax Selects works in the admin and also in public facing forms.
32 + Rich formatting can be easily defined for the dropdown display and the selected "deck" display.
33 + Templates and CSS are fully customizable
34 + JQuery triggers enable you to add javascript to respond when items are added or removed, so other interface elements on the page can react
35 + Default (but customizable) security prevents griefers from pilfering your data via JSON requests
36
37
38
39 Quick Installation
40 ==================
41
42 Get it
43
44 `pip install django-ajax-selects`
45 or
46 `easy_install django-ajax-selects`
47 or
48 download or checkout the distribution
49 or
50 install using buildout by adding `django-ajax-selects` to your `eggs`
51
52 on fedora:
53 su -c 'yum install django-ajax-selects'
54 (note: this version may not be up to date)
55
56
57 In settings.py :
58
59 # add the app
60 INSTALLED_APPS = (
61 ...,
62 'ajax_select'
63 )
64
65 # define the lookup channels in use on the site
66 AJAX_LOOKUP_CHANNELS = {
67 # pass a dict with the model and the field to search against
68 'person' : {'model':'example.person', 'search_field':'name'}
69 }
70 # magically include jqueryUI/js/css
71 AJAX_SELECT_BOOTSTRAP = True
72 AJAX_SELECT_INLINES = 'inline'
73
74 In your urls.py:
75
76 from django.conf.urls.defaults import *
77
78 from django.contrib import admin
79 from ajax_select import urls as ajax_select_urls
80
81 admin.autodiscover()
82
83 urlpatterns = patterns('',
84 # include the lookup urls
85 (r'^admin/lookups/', include(ajax_select_urls)),
86 (r'^admin/', include(admin.site.urls)),
87 )
88
89 In your admin.py:
90
91 from django.contrib import admin
92 from ajax_select import make_ajax_form
93 from ajax_select.admin import AjaxSelectAdmin
94 from example.models import *
95
96 class PersonAdmin(admin.ModelAdmin):
97 pass
98 admin.site.register(Person,PersonAdmin)
99
100 # subclass AjaxSelectAdmin
101 class LabelAdmin(AjaxSelectAdmin):
102 # create an ajax form class using the factory function
103 # model,fieldlist, [form superclass]
104 form = make_ajax_form(Label,{'owner':'person'})
105 admin.site.register(Label,LabelAdmin)
106
107
108 This setup will give most people the ajax powered editing they need by bootstrapping in JS/CSS and implementing default security and simple ajax lookup channels.
109
110 NOT SO QUICK INSTALLATION
111 =========================
112
113 Things that can be customized:
114
115 + how and from where jQuery, jQueryUI, jQueryUI theme are loaded
116 + whether to include js/css inline or for better performance via staticfiles or django-compress etc.
117 + define custom `LookupChannel` classes to customize:
118 + HTML formatting for the drop down results and the item-selected display
119 + custom search queries, ordering, user specific filtered results
120 + custom channel security (default is staff only)
121 + customizing the CSS
122 + each channel could define its own template to change display or add extra javascript
123 + custom javascript can respond to jQuery triggers when items are selected or removed
124
125
126 Architecture
127 ============
128
129 A single view services all of the ajax search requests, delegating the searches to named 'channels'. Each model that needs to be searched for has a channel defined for it. More than one channel may be defined for a Model to serve different needs such as public vs admin or channels that filter the query by specific categories etc. The channel also has access to the request and the user so it can personalize the query results. Those channels can be reused by any Admin that wishes to lookup that model for a ManyToMany or ForeignKey field.
130
131 A simple channel can be specified in settings.py, a more complex one (with custom search, formatting, personalization or auth requirements) can be written in a lookups.py file.
132
133 There are three model field types with corresponding form fields and widgets:
134
135 <table>
136 <tr><th>Database field</th><th>Form field</th><th>Form widget</th>
137 <tr><td>models.CharField</td><td>AutoCompleteField</td><td>AutoCompleteWidget</td></tr>
138 <tr><td>models.ForeignKey</td><td>AutoCompleteSelectField</td><td>AutoCompleteSelectWidget</td></tr>
139 <tr><td>models.ManyToManyField</td><td>AutoCompleteSelectMultipleField</td><td>AutoCompleteSelectMultipleWidget</td></tr>
140 </table>
141
142 Generally the helper functions documented below can be used to generate a complete form or an individual field (with widget) for a form. In rare cases you might need to specify the ajax form field explicitly in your Form.
143
144 Example App
145 ===========
146
147 See the example app for a full working admin site with many variations and comments. It installs quickly using virtualenv and sqllite and comes fully configured.
148
149
150 settings.py
151 -----------
152
153 #### AJAX_LOOKUP_CHANNELS
154
155 Defines the available lookup channels.
156
157 + channel_name : {'model': 'app.modelname', 'search_field': 'name_of_field_to_search' }
158 > This will create a channel automatically
159
160 chanel_name : ( 'app.lookups', 'YourLookup' )
161 This points to a custom Lookup channel name YourLookup in app/lookups.py
162
163 AJAX_LOOKUP_CHANNELS = {
164 # channel : dict with settings to create a channel
165 'person' : {'model':'example.person', 'search_field':'name'},
166
167 # channel: ( module.where_lookup_is, ClassNameOfLookup )
168 'song' : ('example.lookups', 'SongLookup'),
169 }
170
171 #### AJAX_SELECT_BOOTSTRAP
172
173 Sets if it should automatically include jQuery/jQueryUI/theme. On large formsets this will cause it to check each time but it will only jQuery the first time.
174
175 + True: [easiest]
176 use jQuery if already present, else use the admin's jQuery else load from google's CDN
177 use jqueryUI if present else load from google's CDN
178 use jqueryUI theme if present else load one from google's CDN
179
180 + False/None/Not set: [default]
181 you should then include jQuery, jqueryUI + theme in your template or js compressor stack
182
183
184 #### AJAX_SELECT_INLINES
185
186 This controls if and how these:
187
188 ajax_select/static/js/ajax_select.js
189 ajax_select/static/css/ajax_select.css
190
191 are included inline in the html with each form field.
192
193 + 'inline': [easiest]
194 Includes the js and css inline
195 This gets you up and running easily and is fine for small sites.
196 But with many form fields this will be less efficient.
197
198 + 'staticfiles':
199 @import the css/js from {{STATIC_URL}}/ajax_selects using `django.contrib.staticfiles`
200 Requires staticfiles to be installed and to run its management command to collect files.
201 This still imports the css/js multiple times and is thus inefficient but otherwise harmless.
202
203 When using staticfiles you may implement your own `ajax_select.css` and customize to taste as long
204 as your app is before ajax_select in the INSTALLED_APPS.
205
206 + False/None: [default]
207 Does not inline anything. You should include the css/js files in your compressor stack
208 or include them in the head of the admin/base_site.html template.
209 This is the most efficient but takes the longest to configure.
210
211
212 urls.py
213 -------
214
215 Simply include the ajax_select urls in your site's urlpatterns:
216
217 from django.conf.urls.defaults import *
218
219 from django.contrib import admin
220 from ajax_select import urls as ajax_select_urls
221
222 admin.autodiscover()
223
224 urlpatterns = patterns('',
225 (r'^admin/lookups/', include(ajax_select_urls)),
226 (r'^admin/', include(admin.site.urls)),
227 )
228
229
230 lookups.py
231 ----------
232
233 By convention this is where you would define custom lookup channels
234
235 Subclass `LookupChannel` and override any method you wish to customize.
236
237 1.1x Upgrade note: previous versions did not have a parent class. The methods format_result and format_item have been renamed to format_match and format_item_display respectively.
238 Those old lookup channels will still work and the previous methods will be used. It is still better to adjust your lookup channels to inherit from the new base class.
239
240 from ajax_select import LookupChannel
241 from django.utils.html import escape
242 from django.db.models import Q
243 from example.models import *
244
245 class PersonLookup(LookupChannel):
246
247 model = Person
248
249 def get_query(self,q,request):
250 return Person.objects.filter(Q(name__icontains=q) | Q(email__istartswith=q)).order_by('name')
251
252 def get_result(self,obj):
253 u""" result is the simple text that is the completion of what the person typed """
254 return obj.name
255
256 def format_match(self,obj):
257 """ (HTML) formatted item for display in the dropdown """
258 return self.format_item_display(obj)
259
260 def format_item_display(self,obj):
261 """ (HTML) formatted item for displaying item in the selected deck area """
262 return u"%s<div><i>%s</i></div>" % (escape(obj.name),escape(obj.email))
263
264 Note that raw strings should always be escaped with the escape() function
265
266 #### Methods you can override in your `LookupChannel`
267
268
269 ###### model [property]
270
271 The model class this channel searches
272
273 ###### min_length [property, default=1]
274
275 Minimum query length to return a result. Large datasets can choke if they search too often with small queries.
276 Better to demand at least 2 or 3 characters.
277 This param is also used in jQuery's UI when filtering results from its own cache.
278
279 ###### search_field [property, optional]
280
281 Name of the field for the query to search with icontains. This is used only in the default get_query implementation.
282 Usually better to just implement your own get_query
283
284 ###### get_query(self,q,request)
285
286 return a query set searching for the query string q, ordering as appropriate.
287 Either implement this method yourself or set the search_field property.
288 Note that you may return any iterable so you can even use yield and turn this method into a generator,
289 or return an generator or list comprehension.
290
291 ###### get_result(self,obj):
292
293 The text result of autocompleting the entered query. This is currently displayed only for a moment in the text field
294 after the user has selected the item. Then the item is displayed in the item_display deck and the text field is cleared.
295 Future versions may offer different handlers for how to display the selected item(s). In the current version you may
296 add extra script and use triggers to customize.
297
298 ###### format_match(self,obj):
299
300 (HTML) formatted item for displaying item in the result dropdown
301
302 ###### format_item_display(self,obj):
303
304 (HTML) formatted item for displaying item in the selected deck area (directly below the text field).
305 Note that we use jQuery .position() to correctly place the deck area below the text field regardless of
306 whether the widget is in the admin, and admin inline or an outside form. ie. it does not depend on django's
307 admin css to correctly place the selected display area.
308
309 ###### get_objects(self,ids):
310
311 Get the currently selected objects when editing an existing model
312
313 Note that the order of the ids supplied for ManyToMany fields is dependent on how the objects manager fetches it.
314 ie. what is returned by yourmodel.fieldname_set.all()
315
316 In most situations (especially postgres) this order is random, not the order that you originally added them in the interface. With a bit of hacking I have convinced it to preserve the order [see OrderedManyToMany.md for solution]
317
318 ###### can_add(self,user,argmodel):
319
320 Check if the user has permission to add one of these models.
321 This enables the green popup +
322 Default is the standard django permission check
323
324 ###### check_auth(self,request):
325
326 To ensure that nobody can get your data via json simply by knowing the URL.
327 The default is to limit it to request.user.is_staff and raise a PermissionDenied exception.
328 By default this is an error with a 401 response, but your middleware may intercept and choose to do other things.
329
330 Public facing forms should write a custom `LookupChannel` to implement as needed.
331 Also you could choose to return HttpResponseForbidden("who are you?") instead of raising PermissionDenied
332
333
334 admin.py
335 --------
336
337 #### make_ajax_form(model,fieldlist,superclass=ModelForm,show_m2m_help=False)
338
339 If your application does not otherwise require a custom Form class then you can use the make_ajax_form helper to create the entire form directly in admin.py. See forms.py below for cases where you wish to make your own Form.
340
341 + *model*: your model
342 + *fieldlist*: a dict of {fieldname : channel_name, ... }
343 + *superclass*: [default ModelForm] Substitute a different superclass for the constructed Form class.
344 + *show_m2m_help*: [default False]
345 Leave blank [False] if using this form in a standard Admin.
346 Set it True for InlineAdmin classes or if making a form for use outside of the Admin.
347 See discussion below re: Help Text
348
349 ######Example
350
351 from ajax_select import make_ajax_form
352 from ajax_select.admin import AjaxSelectAdmin
353 from yourapp.models import YourModel
354
355 class YourModelAdmin(AjaxSelectAdmin):
356 # create an ajax form class using the factory function
357 # model,fieldlist, [form superclass]
358 form = make_ajax_form(Label,{'owner':'person'})
359
360 admin.site.register(YourModel,YourModelAdmin)
361
362 You may use AjaxSelectAdmin as a mixin class and multiple inherit if you have another Admin class that you would like to use. You may also just add the hook into your own Admin class:
363
364 def get_form(self, request, obj=None, **kwargs):
365 form = super(YourAdminClass,self).get_form(request,obj,**kwargs)
366 autoselect_fields_check_can_add(form,self.model,request.user)
367 return form
368
369 Note that ajax_selects does not need to be in an admin. Popups will still use an admin view (the registered admin for the model being added), even if the form from where the popup was launched does not.
370
371
372 forms.py
373 --------
374
375 subclass ModelForm just as usual. You may add ajax fields using the helper or directly.
376
377 #### make_ajax_field(model,model_fieldname,channel,show_m2m_help = False,**kwargs)
378
379 A factory function to makes an ajax field + widget. The helper ensures things are set correctly and simplifies usage and imports thus reducing programmer error. All kwargs are passed into the Field so it is no less customizable.
380
381 + *model*: the model that this ModelForm is for
382 + *model_fieldname*: the field on the model that is being edited (ForeignKey, ManyToManyField or CharField)
383 + *channel*: the lookup channel to use for searches
384 + *show_m2m_help*: [default False] When using in the admin leave this as False.
385 When using in AdminInline or outside of the admin then set it to True.
386 see Help Text section below.
387 + *kwargs*: Additional kwargs are passed on to the form field.
388 Of interest:
389 help_text="Custom help text"
390 or:
391 # do not show any help at all
392 help_text=None
393
394 #####Example
395
396 from ajax_select import make_ajax_field
397
398 class ReleaseForm(ModelForm):
399
400 class Meta:
401 model = Release
402
403 group = make_ajax_field(Release,'group','group',help_text=None)
404
405 #### Without using the helper
406
407
408 from ajax_select.fields import AutoCompleteSelectField
409
410 class ReleaseForm(ModelForm):
411
412 group = AutoCompleteSelectField('group', required=False, help_text=None)
413
414
415 #### Using ajax selects in a `FormSet`
416
417 There is possibly a better way to do this, but here is an initial example:
418
419 `forms.py`
420
421 from django.forms.models import modelformset_factory
422 from django.forms.models import BaseModelFormSet
423 from ajax_select.fields import AutoCompleteSelectMultipleField, AutoCompleteSelectField
424
425 from models import *
426
427 # create a superclass
428 class BaseTaskFormSet(BaseModelFormSet):
429
430 # that adds the field in, overwriting the previous default field
431 def add_fields(self, form, index):
432 super(BaseTaskFormSet, self).add_fields(form, index)
433 form.fields["project"] = AutoCompleteSelectField('project', required=False)
434
435 # pass in the base formset class to the factory
436 TaskFormSet = modelformset_factory(Task,fields=('name','project','area'),extra=0,formset=BaseTaskFormSet)
437
438
439
440 templates/
441 ----------
442
443 Each form field widget is rendered using a template. You may write a custom template per channel and extend the base template in order to implement these blocks:
444
445 {% block extra_script %}{% endblock %}
446 {% block help %}{% endblock %}
447
448 <table>
449 <tr><th>form Field</th><th>tries this first</th><th>default template</th></tr>
450 <tr><td>AutoCompleteField</td><td>templates/autocomplete_{{CHANNELNAME}}.html</td><td>templates/autocomplete.html</td></tr> <tr><td>AutoCompleteSelectField</td><td>templates/autocompleteselect_{{CHANNELNAME}}.html</td><td>templates/autocompleteselect.html</td></tr>
451 <tr><td>AutoCompleteSelectMultipleField</td><td>templates/autocompleteselectmultiple_{{CHANNELNAME}}.html</td><td>templates/autocompleteselectmultiple.html</td></tr>
452 </table>
453
454 See ajax_select/static/js/ajax_select.js below for the use of jQuery trigger events
455
456
457 ajax_select/static/css/ajax_select.css
458 --------------------------------------
459
460 If you are using `django.contrib.staticfiles` then you can implement `ajax_select.css` and put your app ahead of ajax_select to cause it to be collected by the management command `collectfiles`.
461
462 If you are doing your own compress stack then of course you can include whatever version you want.
463
464 The display style now uses the jQuery UI theme and actually I find the drop down to be not very charming. The previous version (1.1x) which used the external jQuery AutoComplete plugin had nicer styling. I might decide to make the default more like that with alternating color rows and a stronger sense of focused item. Also the current jQuery one wiggles.
465
466 The CSS refers to one image that is served from github (as a CDN):
467 !['https://github.com/crucialfelix/django-ajax-selects/raw/master/ajax_select/static/images/loading-indicator.gif'](https://github.com/crucialfelix/django-ajax-selects/raw/master/ajax_select/static/images/loading-indicator.gif) 'https://github.com/crucialfelix/django-ajax-selects/raw/master/ajax_select/static/images/loading-indicator.gif'
468
469 Your own site's CSS could redefine that with a stronger declaration to point to whatever you like.
470
471 The trashcan icon comes from the jQueryUI theme by the css classes:
472
473 "ui-icon ui-icon-trash"
474
475 The css declaration:
476
477 .results_on_deck .ui-icon.ui-icon-trash { }
478
479 would be "stronger" than jQuery's style declaration and thus you could make trash look less trashy.
480
481
482 ajax_select/static/js/ajax_select.js
483 ------------------------------------
484
485 You probably don't want to mess with this one. But by using the extra_script block as detailed in templates/ above you can add extra javascript, particularily to respond to event Triggers.
486
487 Triggers are a great way to keep code clean and untangled. see: http://docs.jquery.com/Events/trigger
488
489 Two triggers/signals are sent: 'added' and 'killed'. These are sent to the $("#{{html_id}}_on_deck") element. That is the area that surrounds the currently selected items.
490
491 Extend the template, implement the extra_script block and bind functions that will respond to the trigger:
492
493 ##### multi select:
494
495 {% block extra_script %}
496 $("#{{html_id}}_on_deck").bind('added',function() {
497 id = $("#{{html_id}}").val();
498 alert('added id:' + id );
499 });
500 $("#{{html_id}}_on_deck").bind('killed',function() {
501 current = $("#{{html_id}}").val()
502 alert('removed, current is:' + current);
503 });
504 {% endblock %}
505
506 ##### select:
507
508 {% block extra_script %}
509 $("#{{html_id}}_on_deck").bind('added',function() {
510 id = $("#{{html_id}}").val();
511 alert('added id:' + id );
512 });
513 $("#{{html_id}}_on_deck").bind('killed',function() {
514 alert('removed');
515 });
516 {% endblock %}
517
518 ##### auto-complete text field:
519
520 {% block extra_script %}
521 $('#{{ html_id }}').bind('added',function() {
522 entered = $('#{{ html_id }}').val();
523 alert( entered );
524 });
525 {% endblock %}
526
527 There is no remove as there is no kill/delete button in a simple auto-complete.
528 The user may clear the text themselves but there is no javascript involved. Its just a text field.
529
530
531 Planned Improvements
532 --------------------
533
534 TODO: + pop ups are not working in AdminInlines yet
535
536
537
538 Contributors
539 ------------
540
541 Many thanks to all who found bugs, asked for things, and hassled me to get a new release out. I'm glad people find good use out of the app.
542
543 In particular thanks for help in the 1.2 version: sjrd (Sébastien Doeraene), Brian May
544
545
546 License
547 -------
548
549 Dual licensed under the MIT and GPL licenses:
550 http://www.opensource.org/licenses/mit-license.php
551 http://www.gnu.org/licenses/gpl.html
552
553
+0
-7
README.txt less more
0
1 Enables editing of `ForeignKey`, `ManyToMany` and simple text fields using the Autocomplete - `jQuery` plugin.
2
3 django-ajax-selects will work in any normal form as well as in the admin.
4
5 See docs.txt
6
(New empty file)
ajax_select/._LICENSE.txt less more
Binary diff not shown
ajax_select/.___init__.py less more
Binary diff not shown
ajax_select/._docs.txt less more
Binary diff not shown
ajax_select/._fields.py less more
Binary diff not shown
ajax_select/._models.py less more
Binary diff not shown
ajax_select/._urls.py less more
Binary diff not shown
ajax_select/._views.py less more
Binary diff not shown
0 Copyright (c) 2009 Chris Sattinger
0 Copyright (c) 2009-2011 Chris Sattinger
11
22 Dual licensed under the MIT and GPL licenses:
33 http://www.opensource.org/licenses/mit-license.php
00 """JQuery-Ajax Autocomplete fields for Django Forms"""
1 __version__ = "1.1.4"
1 __version__ = "1.2"
22 __author__ = "crucialfelix"
33 __contact__ = "crucialfelix@gmail.com"
44 __homepage__ = "http://code.google.com/p/django-ajax-selects/"
55
66 from django.conf import settings
7 from django.core.exceptions import ImproperlyConfigured
7 from django.core.exceptions import ImproperlyConfigured, PermissionDenied
88 from django.db.models.fields.related import ForeignKey, ManyToManyField
9 from django.contrib.contenttypes.models import ContentType
910 from django.forms.models import ModelForm
1011 from django.utils.text import capfirst
1112 from django.utils.translation import ugettext_lazy as _, ugettext
1213
1314
14 def make_ajax_form(model,fieldlist,superclass=ModelForm):
15 """ this will create a ModelForm subclass inserting
16 AutoCompleteSelectMultipleField (many to many),
17 AutoCompleteSelectField (foreign key)
18
19 where specified in the fieldlist:
20
21 dict(fieldname='channel',...)
22
15 class LookupChannel(object):
16
17 """Subclass this, setting model and overiding the methods below to taste"""
18
19 model = None
20 min_length = 1
21
22 def get_query(self,q,request):
23 """ return a query set searching for the query string q
24 either implement this method yourself or set the search_field
25 in the LookupChannel class definition
26 """
27 kwargs = { "%s__icontains" % self.search_field : q }
28 return self.model.objects.filter(**kwargs).order_by(self.search_field)
29
30 def get_result(self,obj):
31 """ The text result of autocompleting the entered query """
32 return unicode(obj)
33
34 def format_match(self,obj):
35 """ (HTML) formatted item for displaying item in the dropdown """
36 return unicode(obj)
37
38 def format_item_display(self,obj):
39 """ (HTML) formatted item for displaying item in the selected deck area """
40 return unicode(obj)
41
42 def get_objects(self,ids):
43 """ Get the currently selected objects when editing an existing model """
44 # return in the same order as passed in here
45 # this will be however the related objects Manager returns them
46 # which is not guaranteed to be the same order they were in when you last edited
47 # see OrdredManyToMany.md
48 ids = [int(id) for id in ids]
49 things = self.model.objects.in_bulk(ids)
50 return [things[aid] for aid in ids if things.has_key(aid)]
51
52 def can_add(self,user,argmodel):
53 """ Check if the user has permission to add
54 one of these models. This enables the green popup +
55 Default is the standard django permission check
56 """
57 ctype = ContentType.objects.get_for_model(argmodel)
58 return user.has_perm("%s.add_%s" % (ctype.app_label,ctype.model))
59
60 def check_auth(self,request):
61 """ to ensure that nobody can get your data via json simply by knowing the URL.
62 public facing forms should write a custom LookupChannel to implement as you wish.
63 also you could choose to return HttpResponseForbidden("who are you?")
64 instead of raising PermissionDenied (401 response)
65 """
66 if not request.user.is_staff:
67 raise PermissionDenied
68
69
70
71 def make_ajax_form(model,fieldlist,superclass=ModelForm,show_m2m_help=False):
72 """ Creates a ModelForm subclass with autocomplete fields
73
2374 usage:
2475 class YourModelAdmin(Admin):
2576 ...
26 form = make_ajax_form(YourModel,dict(contacts='contact',author='contact'))
27
28 where 'contacts' is a many to many field, specifying to use the lookup channel 'contact'
29 and
30 where 'author' is a foreign key field, specifying here to also use the lookup channel 'contact'
31
77 form = make_ajax_form(YourModel,{'contacts':'contact','author':'contact'})
78
79 where
80 'contacts' is a ManyToManyField specifying to use the lookup channel 'contact'
81 and
82 'author' is a ForeignKeyField specifying here to also use the lookup channel 'contact'
3283 """
3384
3485 class TheForm(superclass):
86
3587 class Meta:
3688 pass
3789 setattr(Meta, 'model', model)
3890
3991 for model_fieldname,channel in fieldlist.iteritems():
40 f = make_ajax_field(model,model_fieldname,channel)
41
92 f = make_ajax_field(model,model_fieldname,channel,show_m2m_help)
93
4294 TheForm.declared_fields[model_fieldname] = f
4395 TheForm.base_fields[model_fieldname] = f
4496 setattr(TheForm,model_fieldname,f)
4698 return TheForm
4799
48100
49 def make_ajax_field(model,model_fieldname,channel,**kwargs):
50 """ makes an ajax select / multiple select / autocomplete field
51 copying the label and help text from the model's db field
52
101 def make_ajax_field(model,model_fieldname,channel,show_m2m_help = False,**kwargs):
102 """ Makes a single autocomplete field for use in a Form
103
53104 optional args:
54 help_text - note that django's ManyToMany db field will append
55 'Hold down "Control", or "Command" on a Mac, to select more than one.'
56 to your db field's help text.
57 Therefore you are better off passing it in here
58 label - default is db field's verbose name
59 required - default's to db field's (not) blank
60 """
105 help_text - default is the model field's help_text
106 label - default is the model field's verbose name
107 required - default is the model field's (not) blank
108
109 show_m2m_help -
110 Django will show help text in the Admin for ManyToMany fields,
111 in which case the help text should not be shown in side the widget itself
112 or it appears twice.
113
114 ForeignKey fields do not behave like this.
115 ManyToMany inside of admin inlines do not do this. [so set show_m2m_help=True]
116
117 But if used outside of the Admin or in an ManyToMany admin inline then you need the help text.
118 """
61119
62120 from ajax_select.fields import AutoCompleteField, \
63121 AutoCompleteSelectMultipleField, \
68126 label = kwargs.pop('label')
69127 else:
70128 label = _(capfirst(unicode(field.verbose_name)))
129
71130 if kwargs.has_key('help_text'):
72131 help_text = kwargs.pop('help_text')
73132 else:
74 if isinstance(field.help_text,basestring):
133 if isinstance(field.help_text,basestring) and field.help_text:
75134 help_text = _(field.help_text)
76135 else:
77136 help_text = field.help_text
81140 required = not field.blank
82141
83142 if isinstance(field,ManyToManyField):
143 kwargs['show_help_text'] = show_m2m_help
84144 f = AutoCompleteSelectMultipleField(
85145 channel,
86146 required=required,
106166 )
107167 return f
108168
169
170 #################### private ##################################################
171
109172 def get_lookup(channel):
110173 """ find the lookup class for the named channel. this is used internally """
111174 try:
112175 lookup_label = settings.AJAX_LOOKUP_CHANNELS[channel]
113 except (KeyError, AttributeError):
176 except AttributeError:
177 raise ImproperlyConfigured("settings.AJAX_LOOKUP_CHANNELS is not configured")
178 except KeyError:
114179 raise ImproperlyConfigured("settings.AJAX_LOOKUP_CHANNELS not configured correctly for %r" % channel)
115180
116181 if isinstance(lookup_label,dict):
117182 # 'channel' : dict(model='app.model', search_field='title' )
118 # generate a simple channel dynamically
183 # generate a simple channel dynamically
119184 return make_channel( lookup_label['model'], lookup_label['search_field'] )
120 else:
185 else: # a tuple
121186 # 'channel' : ('app.module','LookupClass')
122 # from app.module load LookupClass and instantiate
187 # from app.module load LookupClass and instantiate
123188 lookup_module = __import__( lookup_label[0],{},{},[''])
124189 lookup_class = getattr(lookup_module,lookup_label[1] )
190
191 # monkeypatch older lookup classes till 1.3
192 if not hasattr(lookup_class,'format_match'):
193 setattr(lookup_class, 'format_match',
194 getattr(lookup_class,'format_item',
195 lambda self,obj: unicode(obj)))
196 if not hasattr(lookup_class,'format_item_display'):
197 setattr(lookup_class, 'format_item_display',
198 getattr(lookup_class,'format_item',
199 lambda self,obj: unicode(obj)))
200 if not hasattr(lookup_class,'get_result'):
201 setattr(lookup_class, 'get_result',
202 getattr(lookup_class,'format_result',
203 lambda self,obj: unicode(obj)))
204
125205 return lookup_class()
126206
127207
128 def make_channel(app_model,search_field):
208 def make_channel(app_model,arg_search_field):
129209 """ used in get_lookup
130 app_model : app_name.model_name
131 search_field : the field to search against and to display in search results """
210 app_model : app_name.model_name
211 search_field : the field to search against and to display in search results
212 """
132213 from django.db import models
133214 app_label, model_name = app_model.split(".")
134 model = models.get_model(app_label, model_name)
135
136 class AjaxChannel(object):
137
138 def get_query(self,q,request):
139 """ return a query set searching for the query string q """
140 kwargs = { "%s__icontains" % search_field : q }
141 return model.objects.filter(**kwargs).order_by(search_field)
142
143 def format_item(self,obj):
144 """ format item for simple list of currently selected items """
145 return unicode(obj)
146
147 def format_result(self,obj):
148 """ format search result for the drop down of search results. may include html """
149 return unicode(obj)
150
151 def get_objects(self,ids):
152 """ get the currently selected objects """
153 return model.objects.filter(pk__in=ids).order_by(search_field)
154
155 return AjaxChannel()
156
157
215 themodel = models.get_model(app_label, model_name)
216
217 class MadeLookupChannel(LookupChannel):
218
219 model = themodel
220 search_field = arg_search_field
221
222 return MadeLookupChannel()
223
224
22 from ajax_select.fields import autoselect_fields_check_can_add
33 from django.contrib import admin
44
5
56 class AjaxSelectAdmin(admin.ModelAdmin):
6
7
78 """ in order to get + popup functions subclass this or do the same hook inside of your get_form """
8
9
910 def get_form(self, request, obj=None, **kwargs):
1011 form = super(AjaxSelectAdmin,self).get_form(request,obj,**kwargs)
1112
+0
-395
ajax_select/docs.txt less more
0
1 Enables editing of `ForeignKey`, `ManyToMany` and simple text fields using the Autocomplete - `jQuery` plugin.
2
3 django-ajax-selects will work in any normal form as well as in the admin.
4
5
6 ==User experience==
7
8 selecting...
9
10 http://crucial-systems.com/crucialwww/uploads/posts/selecting.png
11
12 selected.
13
14 http://crucial-systems.com/crucialwww/uploads/posts/selected.png
15
16 The user is presented with a text field. They type a search term or a few letters of a name they are looking for, an ajax request is sent to the server, a search channel returns possible results. Results are displayed as a drop down menu. When an item is selected it is added to a display area just below the text field.
17
18 A single view services all of the ajax search requests, delegating the searches to named 'channels'.
19
20 A channel is a simple class that handles the actual searching, defines how you want to treat the query input (split first name and last name, which fields to search etc.) and returns id and formatted results back to the view which sends it to the browser.
21
22 For instance the search channel 'contacts' would search for Contact models. The class would be named ContactLookup. This channel can be used for both AutoCompleteSelect ( foreign key, single item ) and AutoCompleteSelectMultiple (many to many) fields.
23
24 Simple search channels can also be automatically generated, you merely specify the model and the field to search against (see examples below).
25
26 Custom search channels can be written when you need to do a more complex search, check the user's permissions, format the results differently or customize the sort order of the results.
27
28
29 ==Requirements==
30
31 * Django 1.0 +
32 * jquery 1.26 +
33 * Autocomplete - jQuery plugin 1.1 [http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/]
34 * jquery.autocomplete.css (included with Autocomplete)
35
36 The Autocomplete jQuery plugin has now been merged into jQuery UI 1.8 and been improved (2010-06-23). I will migrate this package to use the jQuery UI version and release that as django_ajax_select 1.2, but as of this reading (you, reading this right now) you should download and use his 1.1 version.
37
38
39 ==Installation==
40
41 `pip install django-ajax-selects`
42 or
43 `easy_install django-ajax-selects`
44 or
45 download or checkout the distribution
46 or install using buildout by adding `django-ajax-selects` to your `eggs`
47
48 in settings.py :
49
50 {{{
51 INSTALLED_APPS = (
52 ...,
53 'ajax_select'
54 )
55 }}}
56
57
58 Make sure that these js/css files appear on your page:
59
60 * jquery-1.2.6.js or greater
61 * jquery.autocomplete.js
62 * jquery.autocomplete.css
63 * ajax_select.js (for pop up admin support)
64 * iconic.css (optional, or use this as a starting point)
65
66 I like to use django-compress:
67
68 {{{
69 COMPRESS_CSS = {
70 'all': {
71 'source_filenames': (
72 ...
73 'shared/js/jqplugins/jquery.autocomplete.css',
74 ),
75 'output_filename': 'css/all_compressed.css',
76 'extra_context': {
77 'media': 'screen,projection',
78 },
79 },
80 }
81
82 COMPRESS_JS = {
83 'all': {
84 'source_filenames': (
85 'shared/jquery_ui/jquery-1.2.6.js',
86 'shared/js/jqplugins/jquery.autocomplete.js',
87 ),
88 'output_filename': 'js/all_compressed.js',
89 },
90 }
91 }}}
92
93 But it would be nice if js and css files could be included from any path, not just those in the MEDIA_ROOT. You will have to copy or symlink the included files to place them somewhere where they can be served.
94
95
96
97 in your `settings.py` define the channels in use on the site:
98
99 {{{
100 AJAX_LOOKUP_CHANNELS = {
101 # the simplest case, pass a DICT with the model and field to search against :
102 'track' : dict(model='music.track',search_field='title'),
103 # this generates a simple channel
104 # specifying the model Track in the music app, and searching against the 'title' field
105
106 # or write a custom search channel and specify that using a TUPLE
107 'contact' : ('peoplez.lookups', 'ContactLookup'),
108 # this specifies to look for the class `ContactLookup` in the `peoplez.lookups` module
109 }
110 }}}
111
112 Custom search channels can be written when you need to do a more complex search, check the user's permissions (if the lookup URL should even be accessible to them, and then to perhaps filter which items they are allowed to see), format the results differently or customize the sort order of the results. Search channel objects should implement the 4 methods shown in the following example.
113
114 `peoplez/lookups.py`
115 {{{
116 from peoplez.models import Contact
117 from django.db.models import Q
118
119 class ContactLookup(object):
120
121 def get_query(self,q,request):
122 """ return a query set. you also have access to request.user if needed """
123 return Contact.objects.filter(Q(name__istartswith=q) | Q(fname__istartswith=q) | Q(lname__istartswith=q) | Q(email__icontains=q))
124
125 def format_result(self,contact):
126 """ the search results display in the dropdown menu. may contain html and multiple-lines. will remove any | """
127 return u"%s %s %s (%s)" % (contact.fname, contact.lname,contact.name,contact.email)
128
129 def format_item(self,contact):
130 """ the display of a currently selected object in the area below the search box. html is OK """
131 return unicode(contact)
132
133 def get_objects(self,ids):
134 """ given a list of ids, return the objects ordered as you would like them on the admin page.
135 this is for displaying the currently selected items (in the case of a ManyToMany field)
136 """
137 return Contact.objects.filter(pk__in=ids).order_by('name','lname')
138 }}}
139
140 HTML is fine in the result or item format. Newlines and pipe chars will be removed and everything will be escaped properly.
141
142 Example showing security:
143 {{{
144 from django.http import HttpResponseForbidden
145
146 class ContactLookup(object):
147
148 def get_query(self,q,request):
149 """ return a query set. you also have access to request.user if needed """
150 if not request.user.is_authenticated():
151 raise HttpResponseForbidden() # raising an exception, django will catch this and return an Http 403
152 # filtering only this user's contacts
153 return Contact.objects.filter(name__istartswith=q,created_by=request.user)
154
155 ...
156
157 }}}
158
159
160 include the urls in your site's `urls.py`. This adds the lookup view and the pop up admin view.
161
162 {{{
163 (r'^ajax_select/', include('ajax_select.urls')),
164 }}}
165
166
167 ==Example==
168
169 for an example model:
170
171 {{{
172 class ContactMailing(models.Model):
173 """ can mail to multiple contacts, has one author """
174 contacts = models.ManyToManyField(Contact,blank=True)
175 author = models.ForeignKey(Contact,blank=False)
176 ...
177 }}}
178
179
180 in the `admin.py` for this app:
181
182 {{{
183 from ajax_select import make_ajax_form
184
185 class ContactMailingAdmin(Admin):
186 form = make_ajax_form(ContactMailing,dict(author='contact',contacts='contact'))
187 }}}
188
189 `make_ajax_form( model, fieldlist )` is a factory function which will insert the ajax powered form field inputs
190 so in this example the `author` field (`ForeignKey`) uses the 'contact' channel
191 and the `contacts` field (`ManyToMany`) also uses the 'contact' channel
192
193
194 If you need to write your own form class then specify that form for the admin as usual:
195
196 {{{
197 from forms import ContactMailingForm
198
199 class ContactMailingAdmin(admin.ModelAdmin):
200 form = ContactMailingForm
201
202 admin.site.register(ContactMailing,ContactMailingAdmin)
203 }}}
204
205 in `forms.py` for that app:
206
207 {{{
208 from ajax_select.fields import AutoCompleteSelectMultipleField, AutoCompleteSelectField
209
210 class ContactMailingForm(models.ModelForm):
211
212 # declare a field and specify the named channel that it uses
213 contacts = AutoCompleteSelectMultipleField('contact', required=False)
214 author = AutoCompleteSelectField('contact', required=False)
215
216 }}}
217
218
219 ==Add another via popup==
220
221 Note that ajax_selects does not need to be in an admin. Popups will still use an admin view (the registered admin for the model being added), even if your form does not.
222
223 1. subclass `AjaxSelectAdmin` or include the `autoselect_fields_check_can_add` hook in your admin's `get_form()` [see AjaxSelectAdmin]
224
225 {{{
226 def get_form(self, request, obj=None, **kwargs):
227 form = super(AjaxSelectAdmin,self).get_form(request,obj,**kwargs)
228 autoselect_fields_check_can_add(form,self.model,request.user)
229 return form
230 }}}
231
232 2. Make sure that `js/ajax_select.js` is included in your admin's media or in your site's admin js stack.
233
234
235 `autoselect_fields_check_can_add(form,model,user)`
236
237 This checks if the user has permission to add the model,
238 delegating first to the channel if that implements `can_add(user,model)`
239 otherwise using django's standard user.has_perm check.
240
241 The pop up is served by a custom view that uses the model's registered admin
242
243 3. For this to work you must include ajax_select/urls.py in your root urls.py under this directory:
244
245 `(r'^ajax_select/', include('ajax_select.urls')),`
246
247
248 Once the related object is successfully added, the mischevious custom view hijacks the little javascript response and substitutes `didAddPopup(win,newId,newRepr)` which is in `ajax_select.js`
249
250 Integrating with Django's normal popup admin system is tricky for a number of reasons.
251
252 `ModelAdmin` creates default fields for each field on the model. Then for `ForeignKey` and `ManyToMany` fields it wraps the (default) form field's widget with a `RelatedFieldWidgetWrapper` that adds the magical green +. (Incidentally it adds this regardless of whether you have permission to add the model or not. This is a bug I need to file)
253
254 It then overwrites all of those with any explicitly declared fields. `AutoComplete` fields are declared fields on your form, so if there was a Select field with a wrapper, it gets overwritten by the `AutoCompleteSelect`. That doesn't matter anyway because `RelatedFieldWidgetWrapper` operates only with the default `SelectField` that it is expecting.
255
256 The green + pops open a window with a GET param: `_popup=1`. The `ModelAdmin` recognizes this, the template uses if statements to reduce the page's html a bit, and when the ModelAdmin saves, it returns a simple response with just some javascript that calls `dismissAddAnotherPopup(win, newId, newRepr)` which is a function in `RelatedObjects.js`. That looks for the form field, and if it is a `SelectField` as expected then it alters that accordingly. Then it shuts the pop up window.
257
258
259
260
261 ==Using ajax selects in a `FormSet`==
262
263 There might be a better way to do this.
264
265 `forms.py`
266 {{{
267 from django.forms.models import modelformset_factory
268 from django.forms.models import BaseModelFormSet
269 from ajax_select.fields import AutoCompleteSelectMultipleField, AutoCompleteSelectField
270
271 from models import *
272
273 # create a superclass
274 class BaseTaskFormSet(BaseModelFormSet):
275
276 # that adds the field in, overwriting the previous default field
277 def add_fields(self, form, index):
278 super(BaseTaskFormSet, self).add_fields(form, index)
279 form.fields["project"] = AutoCompleteSelectField('project', required=False)
280
281 # pass in the base formset class to the factory
282 TaskFormSet = modelformset_factory(Task,fields=('name','project','area'),extra=0,formset=BaseTaskFormSet)
283 }}}
284
285
286 ==customizing the html or javascript==
287
288 django's `select_template` is used to choose the template to render the widget's interface:
289
290 `autocompleteselect_{channel}.html` or `autocompleteselect.html`
291
292 So by writing a template `autocompleteselect_{channel}.html` you can customize the interface just for that channel.
293
294
295 ==Handlers: On item added or removed==
296
297 Triggers are a great way to keep code clean and untangled. Two triggers/signals are sent: 'added' and 'killed'. These are sent to the p 'on deck' element. That is the area that surrounds the currently selected items. Its quite easy to bind functions to respond to these triggers.
298
299 Extend the template, implement the extra_script block and bind functions that will respond to the trigger:
300
301 multi select:
302 {{{
303 {% block extra_script %}
304 $("#{{html_id}}_on_deck").bind('added',function() {
305 id = $("#{{html_id}}").val();
306 alert('added id:' + id );
307 });
308 $("#{{html_id}}_on_deck").bind('killed',function() {
309 current = $("#{{html_id}}").val()
310 alert('removed, current is:' + current);
311 });
312 {% endblock %}
313 }}}
314
315 select:
316 {{{
317 {% block extra_script %}
318 $("#{{html_id}}_on_deck").bind('added',function() {
319 id = $("#{{html_id}}").val();
320 alert('added id:' + id );
321 });
322 $("#{{html_id}}_on_deck").bind('killed',function() {
323 alert('removed');
324 });
325 {% endblock %}
326 }}}
327
328 auto-complete text select
329 {{{
330 {% block extra_script %}
331 $('#{{ html_id }}').bind('added',function() {
332 entered = $('#{{ html_id }}').val();
333 alert( entered );
334 });
335 {% endblock %}
336 }}}
337
338 There is no remove as there is no kill/delete button. The user may clear the text themselves but there is no javascript involved. Its just a text field.
339
340
341 see:
342 http://docs.jquery.com/Events/trigger
343
344
345 ==Help text==
346
347 If you are using AutoCompleteSelectMultiple outside of the admin then pass in `show_help_text=True`.
348
349 This is because the admin displays the widget's help text and the widget would also. But when used outside of the admin you need the help text. This is not the case for `AutoCompleteSelect`.
350
351 When defining a db ManyToMany field django will append 'Hold down "Control", or "Command" on a Mac, to select more than one.' regardless of what widget is actually used. http://code.djangoproject.com/ticket/12359
352
353 Thus you should always define the help text in your form field, and its usually nicer to tell people what fields will be searched against. Its not inherently obvious that the text field is "ajax powered" or what a brand of window cleaner has to do with filling out this dang form anyway.
354
355
356 ==CSS==
357
358 See iconic.css for some example styling. autocomplete.js adds the .ac_loading class to the text field while the search is being served. You can style this with fashionable ajax spinning discs etc. A fashionable ajax spinning disc is included.
359
360
361 ==Planned Improvements==
362
363 * Migrating to use the jQuery 1.8 version of autocomplete. This will integrate it with ThemeRoller and would slightly reduce the js codebase.
364
365 * including of media will be improved to use field/admin's Media but it would be preferable if that can be integrated with django-compress
366
367 * make it work within inline many to many fields (when the inlines themselves have lookups)
368
369
370 ==License==
371
372 Copyright (c) 2009 Chris Sattinger
373
374 Dual licensed under the MIT and GPL licenses:
375 http://www.opensource.org/licenses/mit-license.php
376 http://www.gnu.org/licenses/gpl.html
377
378
379 ==Changelog==
380
381 1.1.0
382 Changed AutoCompleteSelect to work like AutoCompleteSelectMultiple:
383 after the result is selected it is displayed below the text input and the text input is cleared.
384 a clickable span is added to remove the item
385 Simplified functions a bit, cleaned up code
386 Added blocks: script and extra_script
387 Added 'killed' and 'added' triggers/signals
388 Support for adding an item via a pop up (ie. the django admin green + sign)
389
390 1.1.4
391 Fixed python 2.4 compatiblity
392 Added LICENSE
393
394
88 from django.template.loader import render_to_string
99 from django.utils.safestring import mark_safe
1010 from django.utils.translation import ugettext as _
11
12
11 import os
12
13
14
15 ####################################################################################
1316
1417 class AutoCompleteSelectWidget(forms.widgets.TextInput):
1518
16 """ widget to select a model """
17
19 """ widget to select a model and return it as text """
20
1821 add_link = None
19
22
2023 def __init__(self,
2124 channel,
2225 help_text='',
3841 obj = objs[0]
3942 except IndexError:
4043 raise Exception("%s cannot find object:%s" % (lookup, value))
41 current_result = mark_safe(lookup.format_item( obj ) )
42 else:
43 current_result = ''
44 display = lookup.format_item_display(obj)
45 current_repr = mark_safe( """new Array("%s",%s)""" % (escapejs(display),obj.pk) )
46 else:
47 current_repr = 'null'
4448
4549 context = {
4650 'name': name,
4751 'html_id' : self.html_id,
52 'min_length': getattr(lookup, 'min_length', 1),
4853 'lookup_url': reverse('ajax_lookup',kwargs={'channel':self.channel}),
4954 'current_id': value,
50 'current_result': current_result,
55 'current_repr': current_repr,
5156 'help_text': self.help_text,
5257 'extra_attrs': mark_safe(flatatt(final_attrs)),
5358 'func_slug': self.html_id.replace("-",""),
5459 'add_link' : self.add_link,
55 'admin_media_prefix' : settings.ADMIN_MEDIA_PREFIX
5660 }
57
61 context.update(bootstrap())
62
5863 return mark_safe(render_to_string(('autocompleteselect_%s.html' % self.channel, 'autocompleteselect.html'),context))
5964
6065 def value_from_datadict(self, data, files, name):
6570 else:
6671 return None
6772
73 def id_for_label(self, id_):
74 return '%s_text' % id_
75
6876
6977
7078 class AutoCompleteSelectField(forms.fields.CharField):
7684 def __init__(self, channel, *args, **kwargs):
7785 self.channel = channel
7886 widget = kwargs.get("widget", False)
87
7988 if not widget or not isinstance(widget, AutoCompleteSelectWidget):
8089 kwargs["widget"] = AutoCompleteSelectWidget(channel=channel,help_text=kwargs.get('help_text',_('Enter text to search.')))
8190 super(AutoCompleteSelectField, self).__init__(max_length=255,*args, **kwargs)
99108 _check_can_add(self,user,model)
100109
101110
111 ####################################################################################
112
102113
103114 class AutoCompleteSelectMultipleWidget(forms.widgets.SelectMultiple):
104115
105116 """ widget to select multiple models """
106
117
107118 add_link = None
108
119
109120 def __init__(self,
110121 channel,
111122 help_text='',
112 show_help_text=False,#admin will also show help. set True if used outside of admin
123 show_help_text=None,
113124 *args, **kwargs):
114125 super(AutoCompleteSelectMultipleWidget, self).__init__(*args, **kwargs)
115126 self.channel = channel
116 self.help_text = help_text
127
128 self.help_text = help_text or _('Enter text to search.')
117129 self.show_help_text = show_help_text
118130
119131 def render(self, name, value, attrs=None):
126138
127139 lookup = get_lookup(self.channel)
128140
129 current_name = "" # the text field starts empty
130141 # eg. value = [3002L, 1194L]
131142 if value:
132143 current_ids = "|" + "|".join( str(pk) for pk in value ) + "|" # |pk|pk| of current
138149 # text repr of currently selected items
139150 current_repr_json = []
140151 for obj in objects:
141 repr = lookup.format_item(obj)
142 current_repr_json.append( """new Array("%s",%s)""" % (escapejs(repr),obj.pk) )
143
152 display = lookup.format_item_display(obj)
153 current_repr_json.append( """new Array("%s",%s)""" % (escapejs(display),obj.pk) )
144154 current_reprs = mark_safe("new Array(%s)" % ",".join(current_repr_json))
155
145156 if self.show_help_text:
146157 help_text = self.help_text
147158 else:
148159 help_text = ''
149
160
150161 context = {
151162 'name':name,
152163 'html_id':self.html_id,
164 'min_length': getattr(lookup, 'min_length', 1),
153165 'lookup_url':reverse('ajax_lookup',kwargs={'channel':self.channel}),
154166 'current':value,
155 'current_name':current_name,
156167 'current_ids':current_ids,
157 'current_reprs':current_reprs,
168 'current_reprs': current_reprs,
158169 'help_text':help_text,
159170 'extra_attrs': mark_safe(flatatt(final_attrs)),
160171 'func_slug': self.html_id.replace("-",""),
161172 'add_link' : self.add_link,
162 'admin_media_prefix' : settings.ADMIN_MEDIA_PREFIX
163173 }
174 context.update(bootstrap())
175
164176 return mark_safe(render_to_string(('autocompleteselectmultiple_%s.html' % self.channel, 'autocompleteselectmultiple.html'),context))
165177
166178 def value_from_datadict(self, data, files, name):
167179 # eg. u'members': [u'|229|4688|190|']
168180 return [long(val) for val in data.get(name,'').split('|') if val]
169181
182 def id_for_label(self, id_):
183 return '%s_text' % id_
170184
171185
172186
178192
179193 def __init__(self, channel, *args, **kwargs):
180194 self.channel = channel
181 help_text = kwargs.get('help_text',_('Enter text to search.'))
195
196 as_default_help = u'Enter text to search.'
197 help_text = kwargs.get('help_text')
198 if not (help_text is None):
199 try:
200 en_help = help_text.translate('en')
201 except AttributeError:
202 pass
203 else:
204 # monkey patch the django default help text to the ajax selects default help text
205 django_default_help = u'Hold down "Control", or "Command" on a Mac, to select more than one.'
206 if django_default_help in en_help:
207 en_help = en_help.replace(django_default_help,'').strip()
208 # probably will not show up in translations
209 if en_help:
210 help_text = _(en_help)
211 else:
212 help_text = _(as_default_help)
213 else:
214 help_text = _(as_default_help)
215
182216 # admin will also show help text, so by default do not show it in widget
183217 # if using in a normal form then set to True so the widget shows help
184 show_help_text = kwargs.get('show_help_text',False)
218 show_help_text = kwargs.pop('show_help_text',False)
219
185220 kwargs['widget'] = AutoCompleteSelectMultipleWidget(channel=channel,help_text=help_text,show_help_text=show_help_text)
221 kwargs['help_text'] = help_text
222
186223 super(AutoCompleteSelectMultipleField, self).__init__(*args, **kwargs)
187224
188225 def clean(self, value):
194231 _check_can_add(self,user,model)
195232
196233
234 ####################################################################################
235
236
197237 class AutoCompleteWidget(forms.TextInput):
198238 """
199239 Widget to select a search result and enter the result as raw text in the text input field.
212252 def render(self, name, value, attrs=None):
213253
214254 value = value or ''
255
215256 final_attrs = self.build_attrs(attrs)
216257 self.html_id = final_attrs.pop('id', name)
217258
259 lookup = get_lookup(self.channel)
260
218261 context = {
219 'current_name': value,
262 'current_repr': mark_safe("'%s'" % escapejs(value)),
220263 'current_id': value,
221264 'help_text': self.help_text,
222265 'html_id': self.html_id,
266 'min_length': getattr(lookup, 'min_length', 1),
223267 'lookup_url': reverse('ajax_lookup', args=[self.channel]),
224268 'name': name,
225269 'extra_attrs':mark_safe(flatatt(final_attrs)),
226 'func_slug': self.html_id.replace("-","")
270 'func_slug': self.html_id.replace("-",""),
227271 }
272 context.update(bootstrap())
228273
229274 templates = ('autocomplete_%s.html' % self.channel,
230275 'autocomplete.html')
231276 return mark_safe(render_to_string(templates, context))
232277
233278
279
234280 class AutoCompleteField(forms.CharField):
235281 """
236282 Field uses an AutoCompleteWidget to lookup possible completions using a channel and stores raw text (not a foreign key)
248294 super(AutoCompleteField, self).__init__(*args, **defaults)
249295
250296
251
252
297 ####################################################################################
253298
254299 def _check_can_add(self,user,model):
255 """ check if the user can add the model, deferring first to the channel if it implements can_add() \
256 else using django's default perm check. \
257 if it can add, then enable the widget to show the + link """
300 """ check if the user can add the model, deferring first to
301 the channel if it implements can_add()
302 else using django's default perm check.
303 if it can add, then enable the widget to show the + link
304 """
258305 lookup = get_lookup(self.channel)
259 try:
306 if hasattr(lookup,'can_add'):
260307 can_add = lookup.can_add(user,model)
261 except AttributeError:
308 else:
262309 ctype = ContentType.objects.get_for_model(model)
263310 can_add = user.has_perm("%s.add_%s" % (ctype.app_label,ctype.model))
264311 if can_add:
265 self.widget.add_link = reverse('add_popup',kwargs={'app_label':model._meta.app_label,'model':model._meta.object_name.lower()})
312 self.widget.add_link = reverse('add_popup',
313 kwargs={'app_label':model._meta.app_label,'model':model._meta.object_name.lower()})
314
266315
267316 def autoselect_fields_check_can_add(form,model,user):
268317 """ check the form's fields for any autoselect fields and enable their widgets with + sign add links if permissions allow"""
271320 db_field = model._meta.get_field_by_name(name)[0]
272321 form_field.check_can_add(user,db_field.rel.to)
273322
323
324 def bootstrap():
325 b = {}
326 b['bootstrap'] = getattr(settings,'AJAX_SELECT_BOOTSTRAP',False)
327 inlines = getattr(settings,'AJAX_SELECT_INLINES',None)
328
329 b['inline'] = ''
330 if inlines == 'inline':
331 directory = os.path.dirname( os.path.realpath(__file__) )
332 f = open(os.path.join(directory,"static","css","ajax_select.css"))
333 css = f.read()
334 f = open(os.path.join(directory,"static","js","ajax_select.js"))
335 js = f.read()
336 b['inline'] = mark_safe(u"""<style type="text/css">%s</style><script type="text/javascript">//<![CDATA[%s//]]></script>""" % (css,js))
337 elif inlines == 'staticfiles':
338 b['inline'] = mark_safe("""<style type="text/css">@import url("%sajax_select/css/ajax_select.css");</style><script type="text/javascript" src="%sajax_select/js/ajax_select.js"></script>""" % (settings.STATIC_URL,settings.STATIC_URL))
339
340 return b
341
342
+0
-48
ajax_select/iconic.css less more
0 /*
1 most of the css that makes it look good is in the jquery.autocomplete.css that comes with the autocomplete plugin.
2 that formats the dropdown of search results.
3
4 one class is used on the html interface itself and that's the X that allows you to remove an item.
5 here is the styling I use. add this to your main css file and season to taste.
6 */
7 .iconic {
8 background: #fff;
9 color: #000;
10 border: 1px solid #ddd;
11 padding: 2px 4px;
12 font-weight: bold;
13 font-family: Courier;
14 text-decoration: none;
15 }
16 .iconic:hover {
17 text-decoration: none;
18 color: #fff;
19 background: #000;
20 cursor: pointer;
21 }
22
23 input.ac_loading {
24 background: #FFF url('../images/loading-indicator.gif') no-repeat;
25 background-position: right;
26 }
27
28
29 /* change the X to an image */
30 .results_on_deck .iconic, .results_on_deck .iconic:hover {
31 float: left;
32 background: url(../shared/images/Trashcan.gif) no-repeat;
33 color: transparent;
34 border: 0;
35 }
36
37 /* specific to a site I worked on. the formatted results were tables. I sized them and floated them left, next to the icon */
38 .results_on_deck div table {
39 float: left;
40 width: 300px;
41 border: 0;
42 }
43 /* and each div in the result clears to start a new row */
44 .results_on_deck > div {
45 clear: both;
46 }
47
+0
-9
ajax_select/js/ajax_select.js less more
0
1 /* requires RelatedObjects.js */
2
3 function didAddPopup(win,newId,newRepr) {
4 var name = windowname_to_id(win.name);
5 $("#"+name).trigger('didAddPopup',[html_unescape(newId),html_unescape(newRepr)]);
6 win.close();
7 }
8
ajax_select/loading-indicator.gif less more
Binary diff not shown
0 # blank file so django recognizes the app
0 # blank file so django recognizes the app
0
1 .results_on_deck .ui-icon-trash {
2 float: left;
3 cursor: pointer;
4 }
5 .results_on_deck {
6 padding: 0.25em 0;
7 }
8 .results_on_deck > div {
9 margin-bottom: 0.5em;
10 }
11 .ui-autocomplete-loading {
12 background: url('https://github.com/crucialfelix/django-ajax-selects/raw/master/ajax_select/static/images/loading-indicator.gif') no-repeat;
13 background-origin: content-box;
14 background-position: right;
15 }
16 ul.ui-autocomplete {
17 /*
18 this is the dropdown menu.
19 max-width: 320px;
20
21 if max-width is not set and you are using django-admin
22 then the dropdown is the width of your whole page body (totally wrong).
23 so set it in your own css to your preferred width of dropdown
24 OR the ajax_select.js will automatically match it to the size of the text field
25 which is what jquery's autocomplete does normally (but django's admin is breaking).
26 tldr: it just works. set max-width if you want it wider
27 */
28 margin: 0;
29 padding: 0;
30 }
31 ul.ui-autocomplete li {
32 list-style-type: none;
33 padding: 0;
34 }
35 ul.ui-autocomplete li a {
36 display: block;
37 padding: 2px 3px;
38 cursor: pointer;
39 }
0
1 if(typeof jQuery.fn.autocompletehtml != 'function') {
2
3 (function($) {
4
5 function _init($deck,$text) {
6 $text.autocompletehtml();
7 if($deck.parents(".module.aligned").length > 0) {
8 // in django-admin, place deck directly below input
9 $deck.position({
10 my: "right top",
11 at: "right bottom",
12 of: $text,
13 offset: "0 5"
14 });
15 }
16 }
17 $.fn.autocompletehtml = function() {
18 var $text = $(this), sizeul = true;
19 this.data("autocomplete")._renderItem = function _renderItemHTML(ul, item) {
20 if(sizeul) {
21 if(ul.css('max-width')=='none') ul.css('max-width',$text.outerWidth());
22 sizeul = false;
23 }
24 return $("<li></li>")
25 .data("item.autocomplete", item)
26 .append("<a>" + item.match + "</a>")
27 .appendTo(ul);
28 };
29 return this;
30 }
31 $.fn.autocompleteselect = function(options) {
32
33 return this.each(function() {
34 var id = this.id;
35 var $this = $(this);
36
37 var $text = $("#"+id+"_text");
38 var $deck = $("#"+id+"_on_deck");
39
40 function receiveResult(event, ui) {
41 if ($this.val()) {
42 kill();
43 }
44 $this.val(ui.item.pk);
45 $text.val('');
46 addKiller(ui.item.repr);
47 $deck.trigger("added");
48
49 return false;
50 }
51
52 function addKiller(repr,pk) {
53 killer_id = "kill_" + pk + id;
54 killButton = '<span class="ui-icon ui-icon-trash" id="'+killer_id+'">X</span> ';
55 if (repr) {
56 $deck.empty();
57 $deck.append("<div>" + killButton + repr + "</div>");
58 } else {
59 $("#"+id+"_on_deck > div").prepend(killButton);
60 }
61 $("#" + killer_id).click(function() {
62 kill();
63 $deck.trigger("killed");
64 });
65 }
66
67 function kill() {
68 $this.val('');
69 $deck.children().fadeOut(1.0).remove();
70 }
71
72 options.select = receiveResult;
73 $text.autocomplete(options);
74 _init($deck,$text);
75
76 if (options.initial) {
77 its = options.initial;
78 addKiller(its[0], its[1]);
79 }
80
81 $this.bind('didAddPopup', function(event, pk, repr) {
82 ui = { item: { pk: pk, repr: repr } }
83 receiveResult(null, ui);
84 });
85 });
86 };
87
88 $.fn.autocompleteselectmultiple = function(options) {
89 return this.each(function() {
90 var id = this.id;
91
92 var $this = $(this);
93 var $text = $("#"+id+"_text");
94 var $deck = $("#"+id+"_on_deck");
95
96 function receiveResult(event, ui) {
97 pk = ui.item.pk;
98 prev = $this.val();
99
100 if (prev.indexOf("|"+pk+"|") == -1) {
101 $this.val((prev ? prev : "|") + pk + "|");
102 addKiller(ui.item.repr, pk);
103 $text.val('');
104 $deck.trigger("added");
105 }
106
107 return false;
108 }
109
110 function addKiller(repr, pk) {
111 killer_id = "kill_" + pk + id;
112 killButton = '<span class="ui-icon ui-icon-trash" id="'+killer_id+'">X</span> ';
113 $deck.append('<div id="'+id+'_on_deck_'+pk+'">' + killButton + repr + ' </div>');
114
115 $("#"+killer_id).click(function() {
116 kill(pk);
117 $deck.trigger("killed");
118 });
119 }
120
121 function kill(pk) {
122 $this.val($this.val().replace("|" + pk + "|", "|"));
123 $("#"+id+"_on_deck_"+pk).fadeOut().remove();
124 }
125
126 options.select = receiveResult;
127 $text.autocomplete(options);
128 _init($deck,$text);
129
130 if (options.initial) {
131 $.each(options.initial, function(i, its) {
132 addKiller(its[0], its[1]);
133 });
134 }
135
136 $this.bind('didAddPopup', function(event, pk, repr) {
137 ui = { item: { pk: pk, repr: repr } }
138 receiveResult(null, ui);
139 });
140 });
141 };
142 })(jQuery);
143
144 function addAutoComplete(prefix_id, callback/*(html_id)*/) {
145 /* detects inline forms and converts the html_id if needed */
146 var prefix = 0;
147 var html_id = prefix_id;
148 if(html_id.indexOf("__prefix__") != -1) {
149 // Some dirty loop to find the appropriate element to apply the callback to
150 while (jQuery('#'+html_id).length) {
151 html_id = prefix_id.replace(/__prefix__/, prefix++);
152 }
153 html_id = prefix_id.replace(/__prefix__/, prefix-2);
154 // Ignore the first call to this function, the one that is triggered when
155 // page is loaded just because the "empty" form is there.
156 if (jQuery("#"+html_id+", #"+html_id+"_text").hasClass("ui-autocomplete-input"))
157 return;
158 }
159 callback(html_id);
160 }
161 /* the popup handler
162 requires RelatedObjects.js which is part of the django admin js
163 so if using outside of the admin then you would need to include that manually */
164 function didAddPopup(win,newId,newRepr) {
165 var name = windowname_to_id(win.name);
166 jQuery("#"+name).trigger('didAddPopup',[html_unescape(newId),html_unescape(newRepr)]);
167 win.close();
168 }
169 }
ajax_select/templates/._autocompleteselectmultiple.html less more
Binary diff not shown
0 <script type="text/javascript">//<![CDATA[
1 if (typeof jQuery == 'undefined') {
2 try { // use django admins
3 jQuery=django.jQuery;
4 } catch(err) {
5 document.write('<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"><\/script>');
6 }
7 }
8 if(typeof jQuery == 'undefined' || (typeof jQuery.ui == 'undefined')) {
9 document.write('<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"><\/script>');
10 document.write('<link type="text/css" rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/smoothness/jquery-ui.css" />');
11 }
12 //]]>
13 </script>
0 {% load i18n %}
1 <input type="text" name="{{ name }}" id="{{ html_id }}" value="{{ current_name }}" {{ extra_attrs }} />
2 {% block help %}{# {% if help_text %}<p class="help">{{ help_text }}</p>{% endif %} #}{% endblock %}
3 <script type="text/javascript">
4 jQuery(document).ready(function($){{% block script %}
5 $('#{{ html_id }}').autocomplete('{{ lookup_url }}', {
6 width: 320,
7 formatItem: function(row) { return row[2]; },
8 formatResult: function(row) { return row[1]; },
9 dataType: "text"
10 })
11 $('#{{ html_id }}').result(function(event, data, formatted) {
12 $('#{{ html_id }}').val(data[1]);
13 $('#{{ html_id }}').trigger("added");
14 })
0 {% if bootstrap %}{% include "ajax_select/bootstrap.html" %}{% endif %}
1 <input type="text" name="{{name}}" id="{{html_id}}" value="{{current_name}}" {{ extra_attrs }} />
2 <script type="text/javascript">//<![CDATA[
3 jQuery(document).ready(function($){
4 {% block script %}
5 addAutoComplete("{{html_id}}", function(html_id) {
6 $("#"+html_id).autocomplete({
7 minLength: {{min_length}},
8 source: '{{lookup_url}}',
9 initial: {{current_repr}},
10 select: function(event, ui) {
11 $("#"+html_id).val(ui.item.value);
12 $("#"+html_id).trigger("added");
13 return false;
14 }
15 }).autocompletehtml();
16 });
1517 {% block extra_script %}{% endblock %}
16 {% endblock %}});
18 {% endblock %}
19 });
20 //]]>
1721 </script>
22 {% block help %}{% if help_text %}<p class="help">{{ help_text }}</p>{% endif %}{% endblock %}
23 {{ inline }}
0 {% load i18n %}
0 {% if bootstrap %}{% include "ajax_select/bootstrap.html" %}{% endif %}
1 <span id="{{ html_id }}_wrapper">
12 <input type="text" name="{{name}}_text" id="{{html_id}}_text" value="" {{ extra_attrs }} />
23 {% if add_link %}
3 <a href="{{ add_link }}" class="add-another" id="add_{{ html_id }}" onclick="return showAddAnotherPopup(this);"> <img src="{{ admin_media_prefix }}img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"></a>
4 <a href="{{ add_link }}" class="add-another addlink" id="add_{{ html_id }}" onclick="return showAddAnotherPopup(this);"> add</a>
45 {% endif %}
5 {% block help %}{# {% if help_text %}<p class="help">{{help_text}}</p>{% endif %} #}{% endblock %}
6 <input type="hidden" name="{{name}}" id="{{html_id}}" value="{{current_id}}" />
67 <div id="{{html_id}}_on_deck" class="results_on_deck"><div>{{current_result|safe}}</div></div>
7 <input type="hidden" name="{{name}}" id="{{html_id}}" value="{{current_id}}" />
8 <script type="text/javascript">
9 jQuery(document).ready(function($){{% block script %}
10 $("#{{html_id}}_text").autocomplete('{{lookup_url}}', {
11 width: 320,
12 formatItem: function(row) { return row[2]; },
13 formatResult: function(row) { return row[1]; },
14 dataType: "text"
15 });
16 function receiveResult(event, data) {
17 prev = $("#{{html_id}}").val();
18 if(prev) {
19 kill_{{ func_slug }}(prev);
20 }
21 $("#{{html_id}}").val(data[0]);
22 $("#{{html_id}}_text").val("");
23 addKiller_{{ func_slug }}(data[1],data[0]);
24 $("#{{html_id}}_on_deck").trigger("added");
25 }
26 $("#{{html_id}}_text").result(receiveResult);
27 function addKiller_{{func_slug}}(repr,id) {
28 kill = "<span class='iconic' id='kill_{{ html_id }}'>X</span> ";
29 if(repr){
30 $( "#{{html_id}}_on_deck" ).empty();
31 $( "#{{html_id}}_on_deck" ).append( "<div>" + kill + repr + "</div>");
32 } else {
33 $( "#{{html_id}}_on_deck > div" ).prepend(kill);
34 }
35 $("#kill_{{ html_id }}").click(function() { return function(){
36 kill_{{func_slug}}();
37 $("#{{html_id}}_on_deck").trigger("killed");
38 }}() );
39 }
40 function kill_{{func_slug}}() {
41 $("#{{html_id}}").val( '' );
42 $( "#{{html_id}}_on_deck" ).children().fadeOut(1.0).remove();
43 }
44 if($("#{{ html_id }}").val()) { // add X for initial value if any
45 addKiller_{{ func_slug }}(null,$("#{{ html_id }}").val());
46 }
47 $("#{{ html_id }}").bind('didAddPopup',function(event,id,repr) {
48 data = Array();
49 data[0] = id;
50 data[1] = repr;
51 receiveResult(null,data);
8 <script type="text/javascript">//<![CDATA[
9 jQuery(document).ready(function($){
10 addAutoComplete("{{html_id}}", function(html_id) {
11 $("#"+html_id).autocompleteselect({
12 minLength: {{min_length}},
13 source: '{{lookup_url}}',
14 initial: {{current_repr}}
15 });
5216 });
5317 {% block extra_script %}{% endblock %}
54 {% endblock %}});
18 });//]]>
5519 </script>
56
20 {% block help %}{% if help_text %}<p class="help">{{help_text}}</p>{% endif %}{% endblock %}
21 </span>
22 {{ inline }}
0 {% load i18n %}
1 <input type="text" name="{{name}}_text" id="{{html_id}}_text" value="{{current_name}}" {{ extra_attrs }} />
0 {% if bootstrap %}{% include "ajax_select/bootstrap.html" %}{% endif %}
1 <input type="text" name="{{name}}_text" id="{{html_id}}_text" value="" {{ extra_attrs }} />
22 {% if add_link %}
3 <a href="{{ add_link }}" class="add-another" id="add_{{ html_id }}" onclick="return showAddAnotherPopup(this);"> <img src="{{ admin_media_prefix }}img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"></a>
3 <a href="{{ add_link }}" class="add-another addlink" id="add_{{ html_id }}" onclick="return showAddAnotherPopup(this);"> add</a>
44 {% endif %}
5 {% block help %}{% if help_text %}<p class="help">{{help_text}}</p>{% endif %}{% endblock %}
6 <p id="{{html_id}}_on_deck" class="results_on_deck"></p>
75 <input type="hidden" name="{{name}}" id="{{html_id}}" value="{{current_ids}}" />
8 <script type="text/javascript">
9 jQuery(document).ready(function($){{% block script %}
10 $("#{{html_id}}_text").autocomplete('{{lookup_url}}', {
11 width: 320,
12 multiple: true,
13 multipleSeparator: " ",
14 scroll: true,
15 scrollHeight: 300,
16 formatItem: function(row) { return row[2]; },
17 formatResult: function(row) { return row[1]; },
18 dataType: "text"
19 });
20 function receiveResult(event, data) {
21 id = data[0];
22 if( $("#{{html_id}}").val().indexOf( "|"+id+"|" ) == -1) {
23 if($("#{{html_id}}").val() == '') {
24 $("#{{html_id}}").val('|');
25 }
26 $("#{{html_id}}").val( $("#{{html_id}}").val() + id + "|");
27 addKiller_{{func_slug}}(data[1],id);
28 $("#{{html_id}}_text").val('');
29 $("#{{html_id}}_on_deck").trigger("added");
30 }
31 }
32 $("#{{html_id}}_text").result(receiveResult);
33 function addKiller_{{func_slug}}(repr,id) {
34 killer_id = "kill_{{ html_id }}" + id
35 kill = "<span class='iconic' id='"+killer_id+"'>X</span> ";
36 $( "#{{html_id}}_on_deck" ).append("<div id='{{html_id}}_on_deck_" + id +"'>" + kill + repr + " </div>");
37 $("#"+killer_id).click(function(frozen_id) { return function(){
38 kill_{{func_slug}}(frozen_id);
39 {# send signal to enclosing p, you may register for this event #}
40 $("#{{html_id}}_on_deck").trigger("killed");
41 }}(id) );
42 }
43 function kill_{{func_slug}}(id) {
44 $("#{{html_id}}").val( $("#{{html_id}}").val().replace( "|" + id + "|", "|" ) );
45 $("#{{html_id}}_on_deck_" + id).fadeOut().remove();
46 }
47 currentRepr = {{current_reprs}};
48 $.each(currentRepr,function(i,its){
49 addKiller_{{func_slug}}(its[0],its[1]);
50 });
51 $("#{{ html_id }}").bind('didAddPopup',function(event,id,repr) {
52 data = Array();
53 data[0] = id;
54 data[1] = repr;
55 receiveResult(null,data);
6 <div id="{{html_id}}_on_deck" class="results_on_deck"></div>
7 <script type="text/javascript">//<![CDATA[
8 jQuery(document).ready(function($){
9 addAutoComplete("{{html_id}}", function(html_id) {
10 $("#"+html_id).autocompleteselectmultiple({
11 minLength: {{min_length}},
12 source: '{{lookup_url}}',
13 initial: {{current_reprs}}
14 });
5615 });
5716 {% block extra_script %}{% endblock %}
58 {% endblock %}});
59 </script>
17 });
18 //]]>
19 </script>
20 {# django admin adds the help text. this is for use outside of the admin #}
21 {% block help %}{% if help_text %}<p class="help">{{help_text}}</p>{% endif %}{% endblock %}
22 {{ inline }}
22 from django.contrib.admin import site
33 from django.db import models
44 from django.http import HttpResponse
5 from django.utils import simplejson
56
67
78 def ajax_lookup(request,channel):
8 """ this view supplies results for both foreign keys and many to many fields """
9
10 """ this view supplies results for foreign keys and many to many fields """
911
1012 # it should come in as GET unless global $.ajaxSetup({type:"POST"}) has been set
1113 # in which case we'll support POST
1214 if request.method == "GET":
1315 # we could also insist on an ajax request
14 if 'q' not in request.GET:
16 if 'term' not in request.GET:
1517 return HttpResponse('')
16 query = request.GET['q']
18 query = request.GET['term']
1719 else:
18 if 'q' not in request.POST:
20 if 'term' not in request.POST:
1921 return HttpResponse('') # suspicious
20 query = request.POST['q']
21
22 lookup_channel = get_lookup(channel)
23
24 if query:
25 instances = lookup_channel.get_query(query,request)
22 query = request.POST['term']
23
24 lookup = get_lookup(channel)
25 if hasattr(lookup,'check_auth'):
26 lookup.check_auth(request)
27
28 if len(query) >= getattr(lookup, 'min_length', 1):
29 instances = lookup.get_query(query,request)
2630 else:
2731 instances = []
2832
29 results = []
30 for item in instances:
31 itemf = lookup_channel.format_item(item)
32 itemf = itemf.replace("\n","").replace("|","&brvbar;")
33 resultf = lookup_channel.format_result(item)
34 resultf = resultf.replace("\n","").replace("|","&brvbar;")
35 results.append( "|".join((unicode(item.pk),itemf,resultf)) )
36 return HttpResponse("\n".join(results))
33 results = simplejson.dumps([
34 {
35 'pk': unicode(getattr(item,'pk',None)),
36 'value': lookup.get_result(item),
37 'match' : lookup.format_match(item),
38 'repr': lookup.format_item_display(item)
39 } for item in instances
40 ])
41
42 return HttpResponse(results, mimetype='application/javascript')
3743
3844
3945 def add_popup(request,app_label,model):
40 """ present an admin site add view, hijacking the result if its the dismissAddAnotherPopup js and returning didAddPopup """
41 themodel = models.get_model(app_label, model)
46 """ this presents the admin site popup add view (when you click the green +)
47
48 make sure that you have added ajax_select.urls to your urls.py:
49 (r'^ajax_select/', include('ajax_select.urls')),
50 this URL is expected in the code below, so it won't work under a different path
51
52 this view then hijacks the result that the django admin returns
53 and instead of calling django's dismissAddAnontherPopup(win,newId,newRepr)
54 it calls didAddPopup(win,newId,newRepr) which was added inline with bootstrap.html
55 """
56 themodel = models.get_model(app_label, model)
4257 admin = site._registry[themodel]
4358
44 admin.admin_site.root_path = "/ajax_select/" # warning: your URL should be configured here.
45 # as in your root urls.py includes :
46 # (r'^ajax_select/', include('ajax_select.urls')),
47 # I should be able to auto-figure this out but ...
59 # TODO : should detect where we really are
60 admin.admin_site.root_path = "/ajax_select/"
4861
4962 response = admin.add_view(request,request.path)
5063 if request.method == 'POST':
51 if response.content.startswith('<script type="text/javascript">opener.dismissAddAnotherPopup'):
64 if 'opener.dismissAddAnotherPopup' in response.content:
5265 return HttpResponse( response.content.replace('dismissAddAnotherPopup','didAddPopup' ) )
5366 return response
5467
0
1 A test application to play with django-ajax-selects
2
3
4 INSTALL ±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±
5
6 Install a local django in a virtualenv:
7
8 ./install.sh
9
10 This will also activate the virtualenv and create a sqlite db
11
12
13 Run the server:
14
15 ./manage.py runserver
16
17 Go visit the admin site and play around:
18
19 http://127.0.0.1:8000/admin
20
21
22 DEACTIVATE ±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±
23
24 To deactiveate the virtualenv just close the shell or:
25
26 deactivate
27
28
29 REACTIVATE ±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±
30
31 Reactivate it later:
32
33 source AJAXSELECTS/bin/activate
34
(New empty file)
0
1 from django.contrib import admin
2 from ajax_select import make_ajax_form
3 from ajax_select.admin import AjaxSelectAdmin
4 from example.forms import ReleaseForm
5 from example.models import *
6
7
8
9 class PersonAdmin(admin.ModelAdmin):
10
11 pass
12
13 admin.site.register(Person,PersonAdmin)
14
15
16
17 class LabelAdmin(AjaxSelectAdmin):
18 """ to get + popup buttons, subclass AjaxSelectAdmin
19
20 multi-inheritance is also possible if you have an Admin class you want to inherit from:
21
22 class PersonAdmin(YourAdminSuperclass,AjaxSelectAdmin):
23
24 this acts as a MixIn to add the relevant methods
25 """
26 # this shows a ForeignKey field
27
28 # create an ajax form class using the factory function
29 # model,fieldlist, [form superclass]
30 form = make_ajax_form(Label,{'owner':'person'})
31
32 admin.site.register(Label,LabelAdmin)
33
34
35
36 class GroupAdmin(AjaxSelectAdmin):
37
38 # this shows a ManyToMany field
39 form = make_ajax_form(Group,{'members':'person'})
40
41 admin.site.register(Group,GroupAdmin)
42
43
44
45 class SongAdmin(AjaxSelectAdmin):
46
47 form = make_ajax_form(Song,{'group':'group','title':'cliche'})
48
49 admin.site.register(Song,SongAdmin)
50
51
52
53 class ReleaseAdmin(AjaxSelectAdmin):
54
55 # specify a form class manually (normal django way)
56 # see forms.py
57 form = ReleaseForm
58
59 admin.site.register(Release,ReleaseAdmin)
60
61
62
63 class BookInline(admin.TabularInline):
64
65 model = Book
66 form = make_ajax_form(Book,{'about_group':'group','mentions_persons':'person'},show_m2m_help=True)
67 extra = 2
68
69 # + check add still not working
70 # no + appearing
71 # def get_formset(self, request, obj=None, **kwargs):
72 # from ajax_select.fields import autoselect_fields_check_can_add
73 # fs = super(BookInline,self).get_formset(request,obj,**kwargs)
74 # autoselect_fields_check_can_add(fs.form,self.model,request.user)
75 # return fs
76
77 class AuthorAdmin(admin.ModelAdmin):
78 inlines = [
79 BookInline,
80 ]
81
82 admin.site.register(Author, AuthorAdmin)
83
84
85
0 # -*- coding: utf-8 -*-
1
2 from django import forms
3 from django.forms.models import ModelForm
4 from ajax_select import make_ajax_field
5 from example.models import Release
6
7
8 class ReleaseForm(ModelForm):
9
10 class Meta:
11 model = Release
12
13 # args: this model, fieldname on this model, lookup_channel_name
14 group = make_ajax_field(Release,'group','group')
15
16 # no help text at all
17 label = make_ajax_field(Release,'label','label',help_text=None)
18
19 # any extra kwargs are passed onto the field, so you may pass a custom help_text here
20 songs = make_ajax_field(Release,'songs','song',help_text=u"Search for song by title")
21
22 # if you are creating a form for use outside of the django admin to specify show_m2m_help=True :
23 # label = make_ajax_field(Release,'label','label',show_m2m_help=True)
24 # so that it will show the help text in manytomany fields
25
26 title = make_ajax_field(Release,'title','cliche',help_text=u"Autocomplete will search the database for clichés about cats.")
27
0
1 # creates a virtualenv and installs a django here
2 virtualenv AJAXSELECTS
3 source AJAXSELECTS/bin/activate
4 easy_install django
5
6 # put ajax selects in the path
7 ln -s ../ajax_select/ ./ajax_select
8
9 # create sqllite database
10 ./manage.py syncdb
11
12 echo "type 'source AJAXSELECTS/bin/activate' to activate the virtualenv"
13 echo "then run: ./manage.py runserver"
14 echo "and visit http://127.0.0.1:8000/admin/"
15 echo "type 'deactivate' to close the virtualenv or just close the shell"
16
0
1
2 from django.db.models import Q
3 from django.utils.html import escape
4 from example.models import *
5 from ajax_select import LookupChannel
6
7
8 class PersonLookup(LookupChannel):
9
10 model = Person
11
12 def get_query(self,q,request):
13 return Person.objects.filter(Q(name__icontains=q) | Q(email__istartswith=q)).order_by('name')
14
15 def get_result(self,obj):
16 u""" result is the simple text that is the completion of what the person typed """
17 return obj.name
18
19 def format_match(self,obj):
20 """ (HTML) formatted item for display in the dropdown """
21 return self.format_item_display(obj)
22
23 def format_item_display(self,obj):
24 """ (HTML) formatted item for displaying item in the selected deck area """
25 return u"%s<div><i>%s</i></div>" % (escape(obj.name),escape(obj.email))
26
27
28
29 class GroupLookup(LookupChannel):
30
31 model = Group
32
33 def get_query(self,q,request):
34 return Group.objects.filter(name__icontains=q).order_by('name')
35
36 def get_result(self,obj):
37 return unicode(obj)
38
39 def format_match(self,obj):
40 return self.format_item_display(obj)
41
42 def format_item_display(self,obj):
43 return u"%s<div><i>%s</i></div>" % (escape(obj.name),escape(obj.url))
44
45 def can_add(self,user,model):
46 """ customize can_add by allowing anybody to add a Group.
47 the superclass implementation uses django's permissions system to check.
48 only those allowed to add will be offered a [+ add] popup link
49 """
50 return True
51
52
53 class SongLookup(LookupChannel):
54
55 model = Song
56
57 def get_query(self,q,request):
58 return Song.objects.filter(title__icontains=q).select_related('group').order_by('title')
59
60 def get_result(self,obj):
61 return unicode(obj.title)
62
63 def format_match(self,obj):
64 return self.format_item_display(obj)
65
66 def format_item_display(self,obj):
67 return "%s<div><i>by %s</i></div>" % (escape(obj.title),escape(obj.group.name))
68
69
70
71 class ClicheLookup(LookupChannel):
72
73 """ an autocomplete lookup does not need to search models
74 though the words here could also be stored in a model and
75 searched as in the lookups above
76 """
77
78 words = [
79 u"rain cats and dogs",
80 u"quick as a cat",
81 u"there's more than one way to skin a cat",
82 u"let the cat out of the bag",
83 u"fat cat",
84 u"the early bird catches the worm",
85 u"catch as catch can",
86 u"you can catch more flies with honey than with vinegar",
87 u"catbird seat",
88 u"cat's paw",
89 u"cat's meow",
90 u"has the cat got your tongue?",
91 u"busy as a cat on a hot tin roof",
92 u"who'll bell the cat",
93 u"cat's ass",
94 u"more nervous than a long tailed cat in a room full of rocking chairs",
95 u"all cats are grey in the dark",
96 u"nervous as a cat in a room full of rocking chairs",
97 u"can't a cat look at a queen?",
98 u"curiosity killed the cat",
99 u"cat's pajamas",
100 u"look what the cat dragged in",
101 u"while the cat's away the mice will play",
102 u"Nervous as a cat in a room full of rocking chairs",
103 u"Slicker than cat shit on a linoleum floor",
104 u"there's more than one way to kill a cat than choking it with butter.",
105 u"you can't swing a dead cat without hitting one",
106 u"The cat's whisker",
107 u"looking like the cat who swallowed the canary",
108 u"not enough room to swing a cat",
109 u"It's raining cats and dogs",
110 u"He was on that like a pack of dogs on a three-legged cat.",
111 u"like two tomcats in a gunny sack",
112 u"I don't know your from adam's house cat!",
113 u"nervous as a long tailed cat in a living room full of rockers",
114 u"Busier than a three legged cat in a dry sand box.",
115 u"Busier than a one-eyed cat watching two mouse holes.",
116 u"kick the dog and cat",
117 u"there's more than one way to kill a cat than to drown it in cream",
118 u"how many ways can you skin a cat?",
119 u"Looks like a black cat with a red bird in its mouth",
120 u"Morals of an alley cat and scruples of a snake.",
121 u"hotter than a six peckered alley cat",
122 u"when the cats are away the mice will play",
123 u"you can catch more flies with honey than vinegar",
124 u"when the cat's away, the mice will play",
125 u"Who opened the cattleguard?",
126 u"your past might catch up with you",
127 u"ain't that just the cats pyjamas",
128 u"A Cat has nine lives",
129 u"a cheshire-cat smile",
130 u"cat's pajamas",
131 u"cat got your tongue?"]
132
133 def get_query(self,q,request):
134 return sorted([w for w in self.words if q in w])
135
136 def get_result(self,obj):
137 return obj
138
139 def format_match(self,obj):
140 return escape(obj)
141
142 def format_item_display(self,obj):
143 return escape(obj)
144
0 #!/usr/bin/env python
1 from django.core.management import execute_manager
2 try:
3 import settings # Assumed to be in the same directory.
4 except ImportError:
5 import sys
6 sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
7 sys.exit(1)
8
9 if __name__ == "__main__":
10 execute_manager(settings)
0
1 from django.db import models
2
3
4 class Person(models.Model):
5
6 """ an actual singular human being """
7 name = models.CharField(blank=True, max_length=100)
8 email = models.EmailField()
9
10 def __unicode__(self):
11 return self.name
12
13
14 class Group(models.Model):
15
16 """ a music group """
17
18 name = models.CharField(max_length=200,unique=True)
19 members = models.ManyToManyField(Person,blank=True,help_text="Enter text to search for and add each member of the group.")
20 url = models.URLField(blank=True, verify_exists=False)
21
22 def __unicode__(self):
23 return self.name
24
25
26 class Label(models.Model):
27
28 """ a record label """
29
30 name = models.CharField(max_length=200,unique=True)
31 owner = models.ForeignKey(Person,blank=True,null=True)
32 url = models.URLField(blank=True, verify_exists=False)
33
34 def __unicode__(self):
35 return self.name
36
37
38 class Song(models.Model):
39
40 """ a song """
41
42 title = models.CharField(blank=False, max_length=200)
43 group = models.ForeignKey(Group)
44
45 def __unicode__(self):
46 return self.title
47
48
49 class Release(models.Model):
50
51 """ a music release/product """
52
53 title = models.CharField(max_length=100)
54 catalog = models.CharField(blank=True, max_length=100)
55
56 group = models.ForeignKey(Group,blank=True,null=True)
57 label = models.ForeignKey(Label,blank=False,null=False)
58 songs = models.ManyToManyField(Song,blank=True)
59
60 def __unicode__(self):
61 return self.title
62
63
64
65 class Author(models.Model):
66 name = models.CharField(max_length=100)
67
68 class Book(models.Model):
69 author = models.ForeignKey(Author)
70 title = models.CharField(max_length=100)
71 about_group = models.ForeignKey(Group)
72 mentions_persons = models.ManyToManyField(Person)
73
0 # Django settings for example project.
1
2 ###########################################################################
3
4 INSTALLED_APPS = (
5 'django.contrib.auth',
6 'django.contrib.contenttypes',
7 'django.contrib.sessions',
8 'django.contrib.sites',
9 'django.contrib.admin',
10 'example',
11
12 ####################################
13 'ajax_select', # <- add the app
14 ####################################
15 )
16
17
18 ###########################################################################
19
20 # DEFINE THE SEARCH CHANNELS:
21
22 AJAX_LOOKUP_CHANNELS = {
23 # simplest way, automatically construct a search channel by passing a dictionary
24 'label' : {'model':'example.label', 'search_field':'name'},
25
26 # Custom channels are specified with a tuple
27 # channel: ( module.where_lookup_is, ClassNameOfLookup )
28 'person' : ('example.lookups', 'PersonLookup'),
29 'group' : ('example.lookups', 'GroupLookup'),
30 'song' : ('example.lookups', 'SongLookup'),
31 'cliche' : ('example.lookups','ClicheLookup')
32 }
33
34
35 AJAX_SELECT_BOOTSTRAP = True
36 # True: [easiest]
37 # use the admin's jQuery if present else load from jquery's CDN
38 # use jqueryUI if present else load from jquery's CDN
39 # use jqueryUI theme if present else load one from jquery's CDN
40 # False/None/Not set: [default]
41 # you should include jQuery, jqueryUI + theme in your template
42
43
44 AJAX_SELECT_INLINES = 'inline'
45 # 'inline': [easiest]
46 # includes the js and css inline
47 # this gets you up and running easily
48 # but on large admin pages or with higher traffic it will be a bit wasteful.
49 # 'staticfiles':
50 # @import the css/js from {{STATIC_URL}}/ajax_selects using django's staticfiles app
51 # requires staticfiles to be installed and to run its management command to collect files
52 # this still includes the css/js multiple times and is thus inefficient
53 # but otherwise harmless
54 # False/None: [default]
55 # does not inline anything. include the css/js files in your compressor stack
56 # or include them in the head of the admin/base_site.html template
57 # this is the most efficient but takes the longest to configure
58
59 # when using staticfiles you may implement your own ajax_select.css and customize to taste
60
61
62
63 ###########################################################################
64
65 # STANDARD CONFIG SETTINGS ###############################################
66
67
68 DEBUG = True
69 TEMPLATE_DEBUG = DEBUG
70
71 ADMINS = (
72 # ('Your Name', 'your_email@domain.com'),
73 )
74
75 MANAGERS = ADMINS
76
77 DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
78 DATABASE_NAME = 'ajax_selects_example' # Or path to database file if using sqlite3.
79 DATABASE_USER = '' # Not used with sqlite3.
80 DATABASE_PASSWORD = '' # Not used with sqlite3.
81 DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
82 DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
83
84 DATABASES = {
85 'default': {
86 'ENGINE': 'django.db.backends.sqlite3',
87 'NAME': 'ajax_selects_example'
88 }
89 }
90
91 # Local time zone for this installation. Choices can be found here:
92 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
93 # although not all choices may be available on all operating systems.
94 # If running in a Windows environment this must be set to the same as your
95 # system time zone.
96 TIME_ZONE = 'America/Chicago'
97
98 # Language code for this installation. All choices can be found here:
99 # http://www.i18nguy.com/unicode/language-identifiers.html
100 LANGUAGE_CODE = 'en-us'
101
102 SITE_ID = 1
103
104 # If you set this to False, Django will make some optimizations so as not
105 # to load the internationalization machinery.
106 USE_I18N = False
107
108 # Absolute path to the directory that holds media.
109 # Example: "/home/media/media.lawrence.com/"
110 MEDIA_ROOT = ''
111
112 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
113 # trailing slash if there is a path component (optional in other cases).
114 # Examples: "http://media.lawrence.com", "http://example.com/media/"
115 MEDIA_URL = ''
116
117 # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
118 # trailing slash.
119 # Examples: "http://foo.com/media/", "/media/".
120 ADMIN_MEDIA_PREFIX = '/media/'
121
122 # Make this unique, and don't share it with nobody.
123 SECRET_KEY = '=9fhrrwrazha6r_m)r#+in*@n@i322ubzy4r+zz%wz$+y(=qpb'
124
125 # List of callables that know how to import templates from various sources.
126 TEMPLATE_LOADERS = (
127 'django.template.loaders.filesystem.load_template_source',
128 'django.template.loaders.app_directories.load_template_source',
129 # 'django.template.loaders.eggs.load_template_source',
130 )
131
132 MIDDLEWARE_CLASSES = (
133 'django.middleware.common.CommonMiddleware',
134 'django.contrib.sessions.middleware.SessionMiddleware',
135 'django.contrib.auth.middleware.AuthenticationMiddleware',
136 )
137
138 ROOT_URLCONF = 'example.urls'
139
140 TEMPLATE_DIRS = (
141 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
142 # Always use forward slashes, even on Windows.
143 # Don't forget to use absolute paths, not relative paths.
144 )
0 from django.conf.urls.defaults import *
1
2 from django.contrib import admin
3 from ajax_select import urls as ajax_select_urls
4
5 admin.autodiscover()
6
7 urlpatterns = patterns('',
8 (r'^admin/lookups/', include(ajax_select_urls)),
9 (r'^admin/', include(admin.site.urls)),
10 )
(New empty file)
22 from distutils.core import setup
33
44 setup(name='django-ajax-selects',
5 version='1.1.4',
6 description='jQuery-powered auto-complete fields for ForeignKey and ManyToMany fields',
5 version='1.2.3',
6 description='jQuery-powered auto-complete fields for editing ForeignKey, ManyToManyField and CharField',
77 author='crucialfelix',
88 author_email='crucialfelix@gmail.com',
99 url='http://code.google.com/p/django-ajax-selects/',
1010 packages=['ajax_select', ],
11 include_package_data = True, # include everything in source control
12 package_data={'ajax_select': ['*.py','*.txt','*.css','*.gif','js/*.js','templates/*.html']},
11 package_data={'ajax_select': ['*.py','*.txt','static/css/*','static/images/*','static/js/*','templates/*.html', 'templates/ajax_select/*.html']},
1312 classifiers = [
1413 "Programming Language :: Python",
1514 "Programming Language :: Python :: 2",
16 "Development Status :: 4 - Beta",
15 "Development Status :: 5 - Production/Stable",
1716 'Environment :: Web Environment',
1817 "Intended Audience :: Developers",
19 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
18 "License :: OSI Approved :: MIT License",
2019 "Operating System :: OS Independent",
2120 "Topic :: Software Development :: Libraries :: Python Modules",
2221 "Topic :: Software Development :: User Interfaces",
2322 "Framework :: Django",
2423 ],
2524 long_description = """\
26 Enables editing of `ForeignKey`, `ManyToMany` and simple text fields using the Autocomplete - `jQuery` plugin.
25 Enables editing of `ForeignKey`, `ManyToManyField` and `CharField` using jQuery UI AutoComplete.
2726
28 django-ajax-selects will work in any normal form as well as in the admin.
27 1. The user types a search term into the text field
28 2. An ajax request is sent to the server.
29 3. The dropdown menu is populated with results.
30 4. User selects by clicking or using arrow keys
31 5. Selected result displays in the "deck" area directly below the input field.
32 6. User can click trashcan icon to remove a selected item
2933
30 The user is presented with a text field. They type a search term or a few letters of a name they are looking for, an ajax request is sent to the server, a search channel returns possible results. Results are displayed as a drop down menu. When an item is selected it is added to a display area just below the text field.
34 + Django 1.2+
35 + Optional boostrap mode allows easy installation by automatic inclusion of jQueryUI from the googleapis CDN
36 + Compatible with staticfiles, appmedia, django-compressor etc
37 + Popup to add a new item is supported
38 + Admin inlines now supported
39 + Ajax Selects works in the admin and also in public facing forms.
40 + Rich formatting can be easily defined for the dropdown display and the selected "deck" display.
41 + Templates and CSS are fully customizable
42 + JQuery triggers enable you to add javascript to respond when items are added or removed, so other interface elements on the page can react
43 + Default (but customizable) security prevents griefers from pilfering your data via JSON requests
3144
3245 """
3346 )