Imported Upstream version 1.2.3
SVN-Git Migration
8 years ago
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 | 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 | ||
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 |
0 | Copyright (c) 2009 Chris Sattinger | |
0 | Copyright (c) 2009-2011 Chris Sattinger | |
1 | 1 | |
2 | 2 | Dual licensed under the MIT and GPL licenses: |
3 | 3 | http://www.opensource.org/licenses/mit-license.php |
0 | 0 | """JQuery-Ajax Autocomplete fields for Django Forms""" |
1 | __version__ = "1.1.4" | |
1 | __version__ = "1.2" | |
2 | 2 | __author__ = "crucialfelix" |
3 | 3 | __contact__ = "crucialfelix@gmail.com" |
4 | 4 | __homepage__ = "http://code.google.com/p/django-ajax-selects/" |
5 | 5 | |
6 | 6 | from django.conf import settings |
7 | from django.core.exceptions import ImproperlyConfigured | |
7 | from django.core.exceptions import ImproperlyConfigured, PermissionDenied | |
8 | 8 | from django.db.models.fields.related import ForeignKey, ManyToManyField |
9 | from django.contrib.contenttypes.models import ContentType | |
9 | 10 | from django.forms.models import ModelForm |
10 | 11 | from django.utils.text import capfirst |
11 | 12 | from django.utils.translation import ugettext_lazy as _, ugettext |
12 | 13 | |
13 | 14 | |
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 | ||
23 | 74 | usage: |
24 | 75 | class YourModelAdmin(Admin): |
25 | 76 | ... |
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' | |
32 | 83 | """ |
33 | 84 | |
34 | 85 | class TheForm(superclass): |
86 | ||
35 | 87 | class Meta: |
36 | 88 | pass |
37 | 89 | setattr(Meta, 'model', model) |
38 | 90 | |
39 | 91 | 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 | ||
42 | 94 | TheForm.declared_fields[model_fieldname] = f |
43 | 95 | TheForm.base_fields[model_fieldname] = f |
44 | 96 | setattr(TheForm,model_fieldname,f) |
46 | 98 | return TheForm |
47 | 99 | |
48 | 100 | |
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 | ||
53 | 104 | 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 | """ | |
61 | 119 | |
62 | 120 | from ajax_select.fields import AutoCompleteField, \ |
63 | 121 | AutoCompleteSelectMultipleField, \ |
68 | 126 | label = kwargs.pop('label') |
69 | 127 | else: |
70 | 128 | label = _(capfirst(unicode(field.verbose_name))) |
129 | ||
71 | 130 | if kwargs.has_key('help_text'): |
72 | 131 | help_text = kwargs.pop('help_text') |
73 | 132 | else: |
74 | if isinstance(field.help_text,basestring): | |
133 | if isinstance(field.help_text,basestring) and field.help_text: | |
75 | 134 | help_text = _(field.help_text) |
76 | 135 | else: |
77 | 136 | help_text = field.help_text |
81 | 140 | required = not field.blank |
82 | 141 | |
83 | 142 | if isinstance(field,ManyToManyField): |
143 | kwargs['show_help_text'] = show_m2m_help | |
84 | 144 | f = AutoCompleteSelectMultipleField( |
85 | 145 | channel, |
86 | 146 | required=required, |
106 | 166 | ) |
107 | 167 | return f |
108 | 168 | |
169 | ||
170 | #################### private ################################################## | |
171 | ||
109 | 172 | def get_lookup(channel): |
110 | 173 | """ find the lookup class for the named channel. this is used internally """ |
111 | 174 | try: |
112 | 175 | 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: | |
114 | 179 | raise ImproperlyConfigured("settings.AJAX_LOOKUP_CHANNELS not configured correctly for %r" % channel) |
115 | 180 | |
116 | 181 | if isinstance(lookup_label,dict): |
117 | 182 | # 'channel' : dict(model='app.model', search_field='title' ) |
118 | # generate a simple channel dynamically | |
183 | # generate a simple channel dynamically | |
119 | 184 | return make_channel( lookup_label['model'], lookup_label['search_field'] ) |
120 | else: | |
185 | else: # a tuple | |
121 | 186 | # 'channel' : ('app.module','LookupClass') |
122 | # from app.module load LookupClass and instantiate | |
187 | # from app.module load LookupClass and instantiate | |
123 | 188 | lookup_module = __import__( lookup_label[0],{},{},['']) |
124 | 189 | 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 | ||
125 | 205 | return lookup_class() |
126 | 206 | |
127 | 207 | |
128 | def make_channel(app_model,search_field): | |
208 | def make_channel(app_model,arg_search_field): | |
129 | 209 | """ 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 | """ | |
132 | 213 | from django.db import models |
133 | 214 | 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 |
2 | 2 | from ajax_select.fields import autoselect_fields_check_can_add |
3 | 3 | from django.contrib import admin |
4 | 4 | |
5 | ||
5 | 6 | class AjaxSelectAdmin(admin.ModelAdmin): |
6 | ||
7 | ||
7 | 8 | """ in order to get + popup functions subclass this or do the same hook inside of your get_form """ |
8 | ||
9 | ||
9 | 10 | def get_form(self, request, obj=None, **kwargs): |
10 | 11 | form = super(AjaxSelectAdmin,self).get_form(request,obj,**kwargs) |
11 | 12 |
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 |
8 | 8 | from django.template.loader import render_to_string |
9 | 9 | from django.utils.safestring import mark_safe |
10 | 10 | from django.utils.translation import ugettext as _ |
11 | ||
12 | ||
11 | import os | |
12 | ||
13 | ||
14 | ||
15 | #################################################################################### | |
13 | 16 | |
14 | 17 | class AutoCompleteSelectWidget(forms.widgets.TextInput): |
15 | 18 | |
16 | """ widget to select a model """ | |
17 | ||
19 | """ widget to select a model and return it as text """ | |
20 | ||
18 | 21 | add_link = None |
19 | ||
22 | ||
20 | 23 | def __init__(self, |
21 | 24 | channel, |
22 | 25 | help_text='', |
38 | 41 | obj = objs[0] |
39 | 42 | except IndexError: |
40 | 43 | 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' | |
44 | 48 | |
45 | 49 | context = { |
46 | 50 | 'name': name, |
47 | 51 | 'html_id' : self.html_id, |
52 | 'min_length': getattr(lookup, 'min_length', 1), | |
48 | 53 | 'lookup_url': reverse('ajax_lookup',kwargs={'channel':self.channel}), |
49 | 54 | 'current_id': value, |
50 | 'current_result': current_result, | |
55 | 'current_repr': current_repr, | |
51 | 56 | 'help_text': self.help_text, |
52 | 57 | 'extra_attrs': mark_safe(flatatt(final_attrs)), |
53 | 58 | 'func_slug': self.html_id.replace("-",""), |
54 | 59 | 'add_link' : self.add_link, |
55 | 'admin_media_prefix' : settings.ADMIN_MEDIA_PREFIX | |
56 | 60 | } |
57 | ||
61 | context.update(bootstrap()) | |
62 | ||
58 | 63 | return mark_safe(render_to_string(('autocompleteselect_%s.html' % self.channel, 'autocompleteselect.html'),context)) |
59 | 64 | |
60 | 65 | def value_from_datadict(self, data, files, name): |
65 | 70 | else: |
66 | 71 | return None |
67 | 72 | |
73 | def id_for_label(self, id_): | |
74 | return '%s_text' % id_ | |
75 | ||
68 | 76 | |
69 | 77 | |
70 | 78 | class AutoCompleteSelectField(forms.fields.CharField): |
76 | 84 | def __init__(self, channel, *args, **kwargs): |
77 | 85 | self.channel = channel |
78 | 86 | widget = kwargs.get("widget", False) |
87 | ||
79 | 88 | if not widget or not isinstance(widget, AutoCompleteSelectWidget): |
80 | 89 | kwargs["widget"] = AutoCompleteSelectWidget(channel=channel,help_text=kwargs.get('help_text',_('Enter text to search.'))) |
81 | 90 | super(AutoCompleteSelectField, self).__init__(max_length=255,*args, **kwargs) |
99 | 108 | _check_can_add(self,user,model) |
100 | 109 | |
101 | 110 | |
111 | #################################################################################### | |
112 | ||
102 | 113 | |
103 | 114 | class AutoCompleteSelectMultipleWidget(forms.widgets.SelectMultiple): |
104 | 115 | |
105 | 116 | """ widget to select multiple models """ |
106 | ||
117 | ||
107 | 118 | add_link = None |
108 | ||
119 | ||
109 | 120 | def __init__(self, |
110 | 121 | channel, |
111 | 122 | help_text='', |
112 | show_help_text=False,#admin will also show help. set True if used outside of admin | |
123 | show_help_text=None, | |
113 | 124 | *args, **kwargs): |
114 | 125 | super(AutoCompleteSelectMultipleWidget, self).__init__(*args, **kwargs) |
115 | 126 | self.channel = channel |
116 | self.help_text = help_text | |
127 | ||
128 | self.help_text = help_text or _('Enter text to search.') | |
117 | 129 | self.show_help_text = show_help_text |
118 | 130 | |
119 | 131 | def render(self, name, value, attrs=None): |
126 | 138 | |
127 | 139 | lookup = get_lookup(self.channel) |
128 | 140 | |
129 | current_name = "" # the text field starts empty | |
130 | 141 | # eg. value = [3002L, 1194L] |
131 | 142 | if value: |
132 | 143 | current_ids = "|" + "|".join( str(pk) for pk in value ) + "|" # |pk|pk| of current |
138 | 149 | # text repr of currently selected items |
139 | 150 | current_repr_json = [] |
140 | 151 | 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) ) | |
144 | 154 | current_reprs = mark_safe("new Array(%s)" % ",".join(current_repr_json)) |
155 | ||
145 | 156 | if self.show_help_text: |
146 | 157 | help_text = self.help_text |
147 | 158 | else: |
148 | 159 | help_text = '' |
149 | ||
160 | ||
150 | 161 | context = { |
151 | 162 | 'name':name, |
152 | 163 | 'html_id':self.html_id, |
164 | 'min_length': getattr(lookup, 'min_length', 1), | |
153 | 165 | 'lookup_url':reverse('ajax_lookup',kwargs={'channel':self.channel}), |
154 | 166 | 'current':value, |
155 | 'current_name':current_name, | |
156 | 167 | 'current_ids':current_ids, |
157 | 'current_reprs':current_reprs, | |
168 | 'current_reprs': current_reprs, | |
158 | 169 | 'help_text':help_text, |
159 | 170 | 'extra_attrs': mark_safe(flatatt(final_attrs)), |
160 | 171 | 'func_slug': self.html_id.replace("-",""), |
161 | 172 | 'add_link' : self.add_link, |
162 | 'admin_media_prefix' : settings.ADMIN_MEDIA_PREFIX | |
163 | 173 | } |
174 | context.update(bootstrap()) | |
175 | ||
164 | 176 | return mark_safe(render_to_string(('autocompleteselectmultiple_%s.html' % self.channel, 'autocompleteselectmultiple.html'),context)) |
165 | 177 | |
166 | 178 | def value_from_datadict(self, data, files, name): |
167 | 179 | # eg. u'members': [u'|229|4688|190|'] |
168 | 180 | return [long(val) for val in data.get(name,'').split('|') if val] |
169 | 181 | |
182 | def id_for_label(self, id_): | |
183 | return '%s_text' % id_ | |
170 | 184 | |
171 | 185 | |
172 | 186 | |
178 | 192 | |
179 | 193 | def __init__(self, channel, *args, **kwargs): |
180 | 194 | 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 | ||
182 | 216 | # admin will also show help text, so by default do not show it in widget |
183 | 217 | # 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 | ||
185 | 220 | kwargs['widget'] = AutoCompleteSelectMultipleWidget(channel=channel,help_text=help_text,show_help_text=show_help_text) |
221 | kwargs['help_text'] = help_text | |
222 | ||
186 | 223 | super(AutoCompleteSelectMultipleField, self).__init__(*args, **kwargs) |
187 | 224 | |
188 | 225 | def clean(self, value): |
194 | 231 | _check_can_add(self,user,model) |
195 | 232 | |
196 | 233 | |
234 | #################################################################################### | |
235 | ||
236 | ||
197 | 237 | class AutoCompleteWidget(forms.TextInput): |
198 | 238 | """ |
199 | 239 | Widget to select a search result and enter the result as raw text in the text input field. |
212 | 252 | def render(self, name, value, attrs=None): |
213 | 253 | |
214 | 254 | value = value or '' |
255 | ||
215 | 256 | final_attrs = self.build_attrs(attrs) |
216 | 257 | self.html_id = final_attrs.pop('id', name) |
217 | 258 | |
259 | lookup = get_lookup(self.channel) | |
260 | ||
218 | 261 | context = { |
219 | 'current_name': value, | |
262 | 'current_repr': mark_safe("'%s'" % escapejs(value)), | |
220 | 263 | 'current_id': value, |
221 | 264 | 'help_text': self.help_text, |
222 | 265 | 'html_id': self.html_id, |
266 | 'min_length': getattr(lookup, 'min_length', 1), | |
223 | 267 | 'lookup_url': reverse('ajax_lookup', args=[self.channel]), |
224 | 268 | 'name': name, |
225 | 269 | 'extra_attrs':mark_safe(flatatt(final_attrs)), |
226 | 'func_slug': self.html_id.replace("-","") | |
270 | 'func_slug': self.html_id.replace("-",""), | |
227 | 271 | } |
272 | context.update(bootstrap()) | |
228 | 273 | |
229 | 274 | templates = ('autocomplete_%s.html' % self.channel, |
230 | 275 | 'autocomplete.html') |
231 | 276 | return mark_safe(render_to_string(templates, context)) |
232 | 277 | |
233 | 278 | |
279 | ||
234 | 280 | class AutoCompleteField(forms.CharField): |
235 | 281 | """ |
236 | 282 | Field uses an AutoCompleteWidget to lookup possible completions using a channel and stores raw text (not a foreign key) |
248 | 294 | super(AutoCompleteField, self).__init__(*args, **defaults) |
249 | 295 | |
250 | 296 | |
251 | ||
252 | ||
297 | #################################################################################### | |
253 | 298 | |
254 | 299 | 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 | """ | |
258 | 305 | lookup = get_lookup(self.channel) |
259 | try: | |
306 | if hasattr(lookup,'can_add'): | |
260 | 307 | can_add = lookup.can_add(user,model) |
261 | except AttributeError: | |
308 | else: | |
262 | 309 | ctype = ContentType.objects.get_for_model(model) |
263 | 310 | can_add = user.has_perm("%s.add_%s" % (ctype.app_label,ctype.model)) |
264 | 311 | 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 | ||
266 | 315 | |
267 | 316 | def autoselect_fields_check_can_add(form,model,user): |
268 | 317 | """ check the form's fields for any autoselect fields and enable their widgets with + sign add links if permissions allow""" |
271 | 320 | db_field = model._meta.get_field_by_name(name)[0] |
272 | 321 | form_field.check_can_add(user,db_field.rel.to) |
273 | 322 | |
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 | /* | |
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 | ||
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 |
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 | } |
Binary diff not shown
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 | }⏎ |
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 | }); | |
15 | 17 | {% block extra_script %}{% endblock %} |
16 | {% endblock %}}); | |
18 | {% endblock %} | |
19 | }); | |
20 | //]]> | |
17 | 21 | </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"> | |
1 | 2 | <input type="text" name="{{name}}_text" id="{{html_id}}_text" value="" {{ extra_attrs }} /> |
2 | 3 | {% 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> | |
4 | 5 | {% 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}}" /> | |
6 | 7 | <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 | }); | |
52 | 16 | }); |
53 | 17 | {% block extra_script %}{% endblock %} |
54 | {% endblock %}}); | |
18 | });//]]> | |
55 | 19 | </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 }} /> | |
2 | 2 | {% 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> | |
4 | 4 | {% 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> | |
7 | 5 | <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 | }); | |
56 | 15 | }); |
57 | 16 | {% 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 }}⏎ |
2 | 2 | from django.contrib.admin import site |
3 | 3 | from django.db import models |
4 | 4 | from django.http import HttpResponse |
5 | from django.utils import simplejson | |
5 | 6 | |
6 | 7 | |
7 | 8 | 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 """ | |
9 | 11 | |
10 | 12 | # it should come in as GET unless global $.ajaxSetup({type:"POST"}) has been set |
11 | 13 | # in which case we'll support POST |
12 | 14 | if request.method == "GET": |
13 | 15 | # we could also insist on an ajax request |
14 | if 'q' not in request.GET: | |
16 | if 'term' not in request.GET: | |
15 | 17 | return HttpResponse('') |
16 | query = request.GET['q'] | |
18 | query = request.GET['term'] | |
17 | 19 | else: |
18 | if 'q' not in request.POST: | |
20 | if 'term' not in request.POST: | |
19 | 21 | 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) | |
26 | 30 | else: |
27 | 31 | instances = [] |
28 | 32 | |
29 | results = [] | |
30 | for item in instances: | |
31 | itemf = lookup_channel.format_item(item) | |
32 | itemf = itemf.replace("\n","").replace("|","¦") | |
33 | resultf = lookup_channel.format_result(item) | |
34 | resultf = resultf.replace("\n","").replace("|","¦") | |
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') | |
37 | 43 | |
38 | 44 | |
39 | 45 | 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) | |
42 | 57 | admin = site._registry[themodel] |
43 | 58 | |
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/" | |
48 | 61 | |
49 | 62 | response = admin.add_view(request,request.path) |
50 | 63 | if request.method == 'POST': |
51 | if response.content.startswith('<script type="text/javascript">opener.dismissAddAnotherPopup'): | |
64 | if 'opener.dismissAddAnotherPopup' in response.content: | |
52 | 65 | return HttpResponse( response.content.replace('dismissAddAnotherPopup','didAddPopup' ) ) |
53 | 66 | return response |
54 | 67 |
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 |
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 | ) |
2 | 2 | from distutils.core import setup |
3 | 3 | |
4 | 4 | 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', | |
7 | 7 | author='crucialfelix', |
8 | 8 | author_email='crucialfelix@gmail.com', |
9 | 9 | url='http://code.google.com/p/django-ajax-selects/', |
10 | 10 | 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']}, | |
13 | 12 | classifiers = [ |
14 | 13 | "Programming Language :: Python", |
15 | 14 | "Programming Language :: Python :: 2", |
16 | "Development Status :: 4 - Beta", | |
15 | "Development Status :: 5 - Production/Stable", | |
17 | 16 | 'Environment :: Web Environment', |
18 | 17 | "Intended Audience :: Developers", |
19 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", | |
18 | "License :: OSI Approved :: MIT License", | |
20 | 19 | "Operating System :: OS Independent", |
21 | 20 | "Topic :: Software Development :: Libraries :: Python Modules", |
22 | 21 | "Topic :: Software Development :: User Interfaces", |
23 | 22 | "Framework :: Django", |
24 | 23 | ], |
25 | 24 | 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. | |
27 | 26 | |
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 | |
29 | 33 | |
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 | |
31 | 44 | |
32 | 45 | """ |
33 | 46 | ) |