pax_global_header00006660000000000000000000000064122406735220014515gustar00rootroot0000000000000052 comment=188f04b7ddc3f6c57dac7af709020c787fba92e8 django-ajax-selects-1.3.3/000077500000000000000000000000001224067352200153445ustar00rootroot00000000000000django-ajax-selects-1.3.3/.gitignore000066400000000000000000000000501224067352200173270ustar00rootroot00000000000000*.pyc *.egg-info/ example/AJAXSELECTS/*django-ajax-selects-1.3.3/.jshintrc000066400000000000000000000006401224067352200171710ustar00rootroot00000000000000{ "node": true, "browser": true, "esnext": true, "bitwise": false, "camelcase": false, "curly": true, "eqeqeq": true, "immed": true, "indent": 2, "latedef": true, "newcap": true, "noarg": true, "quotmark": "single", "regexp": true, "undef": true, "unused": true, "strict": true, "trailing": true, "smarttabs": true, "globals": { "angular": false, "window": true } } django-ajax-selects-1.3.3/MANIFEST.in000066400000000000000000000001351224067352200171010ustar00rootroot00000000000000recursive-include example *.py *.sh *.txt prune example/AJAXSELECTS prune example/ajax_selectdjango-ajax-selects-1.3.3/OrderedManyToMany.md000066400000000000000000000063011224067352200212270ustar00rootroot00000000000000 Ordered ManyToMany fields without a full Through model ====================================================== When re-editing a previously saved model that has a ManyToMany field, the order of the recalled ids can be somewhat random. 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. 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. 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. Problem ------- class Agent(models.Model): name = models.CharField(blank=True, max_length=100) class Apartment(models.Model): agents = models.ManyToManyField(Agent) When the AutoCompleteSelectMultipleField saves it does so by saving each relationship in the order they were added in the interface. # this query does not have a guaranteed order (especially on postgres) # and certainly not the order that we added them apartment.agents.all() # this retrieves the joined objects in the order of their id (the join table id) # and thus gets them in the order they were added apartment.agents.through.objects.filter(apt=self).select_related('agent').order_by('id') Temporary Solution ------------------ class AgentOrderedManyToManyField(models.ManyToManyField): """ regardless of using a through class, only the Manager of the related field is used for fetching the objects for many to many interfaces. with postgres especially this means that the sort order is not determinable other than by the related field's manager. this fetches from the join table, then fetches the Agents in the fixed id order the admin ensures that the agents are always saved in the fixed id order that the form was filled out with """ def value_from_object(self,object): from company.models import Agent rel = getattr(object, self.attname) qry = {self.related.var_name:object} qs = rel.through.objects.filter(**qry).order_by('id') aids = qs.values_list('agent_id',flat=True) agents = dict( (a.pk,a) for a in Agent.objects.filter(pk__in=aids) ) try: return [agents[aid] for aid in aids ] except KeyError: raise Exception("Agent is missing: %s > %s" % (aids,agents)) class Apartment(models.Model): agents = AgentOrderedManyToManyField() class AgentLookup(object): def get_objects(self,ids): # now that we have a dependable ordering # we know the ids are in the order they were originally added # return models in original ordering ids = [int(id) for id in ids] agents = dict( (a.pk,a) for a in Agent.objects.filter(pk__in=ids) ) return [agents[aid] for aid in ids] django-ajax-selects-1.3.3/README.md000066400000000000000000000505401224067352200166270ustar00rootroot00000000000000 Enables editing of `ForeignKey`, `ManyToMany` and `CharField` using jQuery UI Autocomplete. User experience =============== selecting: selected: [Note: screen shots are from the older version. Styling has changed slightly] 1. User types a few characters 2. Ajax request sent to the server 3. The dropdown menu shows choices 4. User selects by clicking or using arrow keys 5. Selected result displays in the "deck" area directly below the input field. 6. User can click trashcan icon to remove a selected item Features ======== + Works in any form including the Django Admin + Popup to add a new item + Admin inlines + Compatible with widget/form media, staticfiles, asset compressors etc. + Automatically Loads jQuery UI mode allows easy installation by automatic inclusion of jQueryUI from the googleapis CDN + Customize HTML, CSS and JS + JQuery triggers allow you to customize interface behavior to respond when items are added or removed + Default (but customizable) security prevents griefers from pilfering your data via JSON requests Quick Installation ================== Get it `pip install django-ajax-selects` or download or checkout the distribution In settings.py : # add the app INSTALLED_APPS = ( ..., 'django.contrib.staticfiles', 'ajax_select' ) # define the lookup channels in use on the site AJAX_LOOKUP_CHANNELS = { # simple: search Person.objects.filter(name__icontains=q) 'person' : {'model': 'example.person', 'search_field': 'name'}, # define a custom lookup channel 'song' : ('example.lookups', 'SongLookup') } In your urls.py: from django.conf.urls.defaults import * from django.contrib import admin from ajax_select import urls as ajax_select_urls admin.autodiscover() urlpatterns = patterns('', # include the lookup urls (r'^admin/lookups/', include(ajax_select_urls)), (r'^admin/', include(admin.site.urls)), ) In your admin.py: from django.contrib import admin from ajax_select import make_ajax_form from ajax_select.admin import AjaxSelectAdmin from example.models import * class PersonAdmin(admin.ModelAdmin): pass admin.site.register(Person,PersonAdmin) class SongAdmin(AjaxSelectAdmin): # create an ajax form class using the factory function # model,fieldlist, [form superclass] form = make_ajax_form(Label,{'owner':'person'}) admin.site.register(Label,LabelAdmin) example/lookups.py: from ajax_select import LookupChannel class SongLookup(LookupChannel): model = Song def get_query(self,q,request): return Song.objects.filter(title__icontains=q).order_by('title') NOT SO QUICK INSTALLATION ========================= Things that can be customized: + define custom `LookupChannel` classes to customize: + HTML formatting for the drop down results and the item-selected display + custom search queries, ordering, user specific filtered results + custom channel security (default is staff only) + each channel can define its own template to add controls or javascript + JS can respond to jQuery triggers when items are selected or removed + custom CSS + how and from where jQuery, jQueryUI, jQueryUI theme are loaded Architecture ============ A single view services all of the ajax search requests, delegating the searches to named 'channels'. 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. 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. There are three model field types with corresponding form fields and widgets:
Database fieldForm fieldForm widget
models.CharFieldAutoCompleteFieldAutoCompleteWidget
models.ForeignKeyAutoCompleteSelectFieldAutoCompleteSelectWidget
models.ManyToManyFieldAutoCompleteSelectMultipleFieldAutoCompleteSelectMultipleWidget
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. Example App =========== 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. settings.py ----------- #### AJAX_LOOKUP_CHANNELS Defines the available lookup channels. + channel_name : {'model': 'app.modelname', 'search_field': 'name_of_field_to_search' } > This will create a channel automatically chanel_name : ( 'app.lookups', 'YourLookup' ) This points to a custom Lookup channel name YourLookup in app/lookups.py AJAX_LOOKUP_CHANNELS = { # channel : dict with settings to create a channel 'person' : {'model':'example.person', 'search_field':'name'}, # channel: ( module.where_lookup_is, ClassNameOfLookup ) 'song' : ('example.lookups', 'SongLookup'), } #### AJAX_SELECT_BOOTSTRAP By default it will include bootstrap.js in the widget media which will locate or load jQuery and jQuery-UI. In other words, by default it will just work. If you don't want it do that, in settings.py: AJAX_SELECT_BOOTSTRAP = False First one wins: * window.jQuery - if you already included jQuery on the page * or loads: //ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js Likewise for jQuery-UI: * window.jQuery.ui * or loads: //ajax.googleapis.com/ajax/libs/jqueryui/1.8.24/jquery-ui.min.js with theme: //ajax.googleapis.com/ajax/libs/jqueryui/1.8.24/themes/smoothness/jquery-ui.css If you want your own custom theme then load jquery ui and your css first. Warning: the latest jQueryUI seems to have issues with the autocomplete. I would rather switch to the much nicer select2 than try to get the latest jQuery UI to work. Its a lot of js and css to load just for a dropdown. urls.py ------- Simply include the ajax_select urls in your site's urlpatterns: from django.conf.urls.defaults import * from django.contrib import admin from ajax_select import urls as ajax_select_urls admin.autodiscover() urlpatterns = patterns('', (r'^admin/lookups/', include(ajax_select_urls)), (r'^admin/', include(admin.site.urls)), ) lookups.py ---------- By convention this is where you would define custom lookup channels Subclass `LookupChannel` and override any method you wish to customize. 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. 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. from ajax_select import LookupChannel from django.utils.html import escape from django.db.models import Q from example.models import * class PersonLookup(LookupChannel): model = Person def get_query(self,q,request): return Person.objects.filter(Q(name__icontains=q) | Q(email__istartswith=q)).order_by('name') def get_result(self,obj): u""" result is the simple text that is the completion of what the person typed """ return obj.name def format_match(self,obj): """ (HTML) formatted item for display in the dropdown """ return self.format_item_display(obj) def format_item_display(self,obj): """ (HTML) formatted item for displaying item in the selected deck area """ return u"%s
%s
" % (escape(obj.name),escape(obj.email)) Note that raw strings should always be escaped with the escape() function #### Methods you can override in your `LookupChannel` ###### model [property] The model class this channel searches ###### plugin_options [property, default={}] Set any options for the jQuery plugin. This includes: + minLength + autoFocus + disabled + position + source - setting this would overide the normal ajax URL. could be used to add URL query params See http://docs.jquery.com/UI/Autocomplete#options The field or widget may also specify plugin_options that will overwrite those specified by the channel. ###### min_length [property, default=1] This is a jQuery plugin option. It is preferred to set this in the plugin_options dict, but this older style attribute will still be honored. Minimum query length to return a result. Large datasets can choke if they search too often with small queries. Better to demand at least 2 or 3 characters. This param is also used in jQuery's UI when filtering results from its own cache. ###### search_field [property, optional] Name of the field for the query to search with icontains. This is used only in the default get_query implementation. Usually better to just implement your own get_query ###### get_query(self,q,request) return a query set searching for the query string q, ordering as appropriate. Either implement this method yourself or set the search_field property. Note that you may return any iterable so you can even use yield and turn this method into a generator, or return an generator or list comprehension. ###### get_result(self,obj): The text result of autocompleting the entered query. This is currently displayed only for a moment in the text field after the user has selected the item. Then the item is displayed in the item_display deck and the text field is cleared. Future versions may offer different handlers for how to display the selected item(s). In the current version you may add extra script and use triggers to customize. ###### format_match(self,obj): (HTML) formatted item for displaying item in the result dropdown ###### format_item_display(self,obj): (HTML) formatted item for displaying item in the selected deck area (directly below the text field). Note that we use jQuery .position() to correctly place the deck area below the text field regardless of whether the widget is in the admin, and admin inline or an outside form. ie. it does not depend on django's admin css to correctly place the selected display area. ###### get_objects(self,ids): Get the currently selected objects when editing an existing model Note that the order of the ids supplied for ManyToMany fields is dependent on how the objects manager fetches it. ie. what is returned by yourmodel.fieldname_set.all() 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] ###### can_add(self, user, argmodel): Check if the user has permission to add one of these models. This enables the green popup + Default is the standard django permission check ###### check_auth(self,request): To ensure that nobody can get your data via json simply by knowing the URL. The default is to limit it to request.user.is_staff and raise a PermissionDenied exception. By default this is an error with a 401 response, but your middleware may intercept and choose to do other things. Public facing forms should write a custom `LookupChannel` to implement as needed. Also you could choose to return HttpResponseForbidden("who are you?") instead of raising PermissionDenied admin.py -------- #### make_ajax_form(model, fieldlist, superclass=ModelForm, show_help_text=False) 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. + *model*: your model + *fieldlist*: a dict of {fieldname : channel_name, ... } + *superclass*: [default ModelForm] Substitute a different superclass for the constructed Form class. + *show_help_text*: [default False] Leave blank [False] if using this form in a standard Admin. Set it True for InlineAdmin classes or if making a form for use outside of the Admin. ######Example from ajax_select import make_ajax_form from ajax_select.admin import AjaxSelectAdmin from yourapp.models import YourModel class YourModelAdmin(AjaxSelectAdmin): # create an ajax form class using the factory function # model, fieldlist, [form superclass] form = make_ajax_form(Label, {'owner': 'person'}) admin.site.register(YourModel,YourModelAdmin) 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: def get_form(self, request, obj=None, **kwargs): form = super(YourAdminClass, self).get_form(request, obj, **kwargs) autoselect_fields_check_can_add(form, self.model, request.user) return form 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. forms.py -------- subclass ModelForm just as usual. You may add ajax fields using the helper or directly. #### make_ajax_field(model, model_fieldname, channel, show_help_text=False, **kwargs) 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. + *model*: the model that this ModelForm is for + *model_fieldname*: the field on the model that is being edited (ForeignKey, ManyToManyField or CharField) + *channel*: the lookup channel to use for searches + *show_help_text*: [default False] Whether to show the help text inside the widget itself. When using in AdminInline or outside of the admin then set it to True. + *kwargs*: Additional kwargs are passed on to the form field. Of interest: help_text="Custom help text" or: # do not show any help at all help_text=None plugin_options - directly specify jQuery plugin options. see Lookup plugin_options above #####Example from ajax_select import make_ajax_field class ReleaseForm(ModelForm): class Meta: model = Release group = make_ajax_field(Release, 'group', 'group', help_text=None) #### Without using the helper from ajax_select.fields import AutoCompleteSelectField class ReleaseForm(ModelForm): group = AutoCompleteSelectField('group', required=False, help_text=None) #### Setting plugin options from ajax_select.fields import AutoCompleteSelectField class ReleaseForm(ModelForm): group = AutoCompleteSelectField('group', required=False, help_text=None, plugin_options = {'autoFocus': True, 'minLength': 4}) #### Using ajax selects in a `FormSet` There is possibly a better way to do this, but here is an initial example: `forms.py` from django.forms.models import modelformset_factory from django.forms.models import BaseModelFormSet from ajax_select.fields import AutoCompleteSelectMultipleField, AutoCompleteSelectField from models import * # create a superclass class BaseTaskFormSet(BaseModelFormSet): # that adds the field in, overwriting the previous default field def add_fields(self, form, index): super(BaseTaskFormSet, self).add_fields(form, index) form.fields["project"] = AutoCompleteSelectField('project', required=False) # pass in the base formset class to the factory TaskFormSet = modelformset_factory(Task, fields=('name', 'project', 'area'),extra=0, formset=BaseTaskFormSet) templates/ ---------- 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: {% block extra_script %}{% endblock %} {% block help %}{% endblock %}
form Fieldtries this firstdefault template
AutoCompleteFieldtemplates/autocomplete_{{CHANNELNAME}}.htmltemplates/autocomplete.html
AutoCompleteSelectFieldtemplates/autocompleteselect_{{CHANNELNAME}}.htmltemplates/autocompleteselect.html
AutoCompleteSelectMultipleFieldtemplates/autocompleteselectmultiple_{{CHANNELNAME}}.htmltemplates/autocompleteselectmultiple.html
See ajax_select/static/js/ajax_select.js below for the use of jQuery trigger events ajax_select/static/css/ajax_select.css -------------------------------------- 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`. If you are doing your own compress stack then of course you can include whatever version you want. 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. The trashcan icon comes from the jQueryUI theme by the css classes: "ui-icon ui-icon-trash" The css declaration: .results_on_deck .ui-icon.ui-icon-trash { } would be "stronger" than jQuery's style declaration and thus you could make trash look less trashy. ajax_select/static/js/ajax_select.js ------------------------------------ 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. Triggers are a great way to keep code clean and untangled. see: http://docs.jquery.com/Events/trigger 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. Extend the template, implement the extra_script block and bind functions that will respond to the trigger: ##### multi select: {% block extra_script %} $("#{{html_id}}_on_deck").bind('added',function() { id = $("#{{html_id}}").val(); alert('added id:' + id ); }); $("#{{html_id}}_on_deck").bind('killed',function() { current = $("#{{html_id}}").val() alert('removed, current is:' + current); }); {% endblock %} ##### select: {% block extra_script %} $("#{{html_id}}_on_deck").bind('added',function() { id = $("#{{html_id}}").val(); alert('added id:' + id ); }); $("#{{html_id}}_on_deck").bind('killed',function() { alert('removed'); }); {% endblock %} ##### auto-complete text field: {% block extra_script %} $('#{{ html_id }}').bind('added',function() { entered = $('#{{ html_id }}').val(); alert( entered ); }); {% endblock %} There is no remove as there is no kill/delete button in a simple auto-complete. The user may clear the text themselves but there is no javascript involved. Its just a text field. Contributors ------------ 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. In particular thanks for help in the 1.2 version: @sjrd (Sébastien Doeraene), @brianmay License ------- Dual licensed under the MIT and GPL licenses: http://www.opensource.org/licenses/mit-license.php http://www.gnu.org/licenses/gpl.html django-ajax-selects-1.3.3/__init__.py000066400000000000000000000000001224067352200174430ustar00rootroot00000000000000django-ajax-selects-1.3.3/ajax_select/000077500000000000000000000000001224067352200176265ustar00rootroot00000000000000django-ajax-selects-1.3.3/ajax_select/LICENSE.txt000066400000000000000000000002651224067352200214540ustar00rootroot00000000000000Copyright (c) 2009-2013 Chris Sattinger Dual licensed under the MIT and GPL licenses: http://www.opensource.org/licenses/mit-license.php http://www.gnu.org/licenses/gpl.html django-ajax-selects-1.3.3/ajax_select/__init__.py000066400000000000000000000204011224067352200217340ustar00rootroot00000000000000"""JQuery-Ajax Autocomplete fields for Django Forms""" __version__ = "1.3.3" __author__ = "crucialfelix" __contact__ = "crucialfelix@gmail.com" __homepage__ = "https://github.com/crucialfelix/django-ajax-selects/" from django.conf import settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.db.models.fields.related import ForeignKey, ManyToManyField from django.contrib.contenttypes.models import ContentType from django.forms.models import ModelForm from django.utils.text import capfirst from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ class LookupChannel(object): """Subclass this, setting model and overiding the methods below to taste""" model = None plugin_options = {} min_length = 1 def get_query(self, q, request): """ return a query set searching for the query string q either implement this method yourself or set the search_field in the LookupChannel class definition """ kwargs = {"%s__icontains" % self.search_field: q} return self.model.objects.filter(**kwargs).order_by(self.search_field) def get_result(self, obj): """ The text result of autocompleting the entered query """ return escape(unicode(obj)) def format_match(self, obj): """ (HTML) formatted item for displaying item in the dropdown """ return escape(unicode(obj)) def format_item_display(self, obj): """ (HTML) formatted item for displaying item in the selected deck area """ return escape(unicode(obj)) def get_objects(self, ids): """ Get the currently selected objects when editing an existing model """ # return in the same order as passed in here # this will be however the related objects Manager returns them # which is not guaranteed to be the same order they were in when you last edited # see OrdredManyToMany.md ids = [int(id) for id in ids] things = self.model.objects.in_bulk(ids) return [things[aid] for aid in ids if aid in things] def can_add(self, user, argmodel): """ Check if the user has permission to add one of these models. This enables the green popup + Default is the standard django permission check """ ctype = ContentType.objects.get_for_model(argmodel) return user.has_perm("%s.add_%s" % (ctype.app_label, ctype.model)) def check_auth(self, request): """ to ensure that nobody can get your data via json simply by knowing the URL. public facing forms should write a custom LookupChannel to implement as you wish. also you could choose to return HttpResponseForbidden("who are you?") instead of raising PermissionDenied (401 response) """ if not request.user.is_staff: raise PermissionDenied def make_ajax_form(model, fieldlist, superclass=ModelForm, show_help_text=False, **kwargs): """ Creates a ModelForm subclass with autocomplete fields usage: class YourModelAdmin(Admin): ... form = make_ajax_form(YourModel,{'contacts':'contact','author':'contact'}) where 'contacts' is a ManyToManyField specifying to use the lookup channel 'contact' and 'author' is a ForeignKeyField specifying here to also use the lookup channel 'contact' """ # will support previous arg name for several versions before deprecating if 'show_m2m_help' in kwargs: show_help_text = kwargs.pop('show_m2m_help') class TheForm(superclass): class Meta: pass setattr(Meta, 'model', model) if hasattr(superclass, 'Meta'): if hasattr(superclass.Meta, 'fields'): setattr(Meta, 'fields', superclass.Meta.fields) if hasattr(superclass.Meta, 'exclude'): setattr(Meta, 'exclude', superclass.Meta.exclude) if hasattr(superclass.Meta, 'widgets'): setattr(Meta, 'widgets', superclass.Meta.widgets) for model_fieldname, channel in fieldlist.iteritems(): f = make_ajax_field(model, model_fieldname, channel, show_help_text) TheForm.declared_fields[model_fieldname] = f TheForm.base_fields[model_fieldname] = f setattr(TheForm, model_fieldname, f) return TheForm def make_ajax_field(model, model_fieldname, channel, show_help_text=False, **kwargs): """ Makes a single autocomplete field for use in a Form optional args: help_text - default is the model db field's help_text. None will disable all help text label - default is the model db field's verbose name required - default is the model db field's (not) blank show_help_text - Django will show help text below the widget, but not for ManyToMany inside of admin inlines This setting will show the help text inside the widget itself. """ # will support previous arg name for several versions before deprecating if 'show_m2m_help' in kwargs: show_help_text = kwargs.pop('show_m2m_help') from ajax_select.fields import AutoCompleteField, \ AutoCompleteSelectMultipleField, \ AutoCompleteSelectField field = model._meta.get_field(model_fieldname) if not 'label' in kwargs: kwargs['label'] = _(capfirst(unicode(field.verbose_name))) if not 'help_text' in kwargs and field.help_text: kwargs['help_text'] = field.help_text if not 'required' in kwargs: kwargs['required'] = not field.blank kwargs['show_help_text'] = show_help_text if isinstance(field, ManyToManyField): f = AutoCompleteSelectMultipleField( channel, **kwargs ) elif isinstance(field, ForeignKey): f = AutoCompleteSelectField( channel, **kwargs ) else: f = AutoCompleteField( channel, **kwargs ) return f #################### private ################################################## def get_lookup(channel): """ find the lookup class for the named channel. this is used internally """ try: lookup_label = settings.AJAX_LOOKUP_CHANNELS[channel] except AttributeError: raise ImproperlyConfigured("settings.AJAX_LOOKUP_CHANNELS is not configured") except KeyError: raise ImproperlyConfigured("settings.AJAX_LOOKUP_CHANNELS not configured correctly for %r" % channel) if isinstance(lookup_label, dict): # 'channel' : dict(model='app.model', search_field='title' ) # generate a simple channel dynamically return make_channel(lookup_label['model'], lookup_label['search_field']) else: # a tuple # 'channel' : ('app.module','LookupClass') # from app.module load LookupClass and instantiate lookup_module = __import__(lookup_label[0], {}, {}, ['']) lookup_class = getattr(lookup_module, lookup_label[1]) # monkeypatch older lookup classes till 1.3 if not hasattr(lookup_class, 'format_match'): setattr(lookup_class, 'format_match', getattr(lookup_class, 'format_item', lambda self, obj: unicode(obj))) if not hasattr(lookup_class, 'format_item_display'): setattr(lookup_class, 'format_item_display', getattr(lookup_class, 'format_item', lambda self, obj: unicode(obj))) if not hasattr(lookup_class, 'get_result'): setattr(lookup_class, 'get_result', getattr(lookup_class, 'format_result', lambda self, obj: unicode(obj))) return lookup_class() def make_channel(app_model, arg_search_field): """ used in get_lookup app_model : app_name.model_name search_field : the field to search against and to display in search results """ from django.db import models app_label, model_name = app_model.split(".") themodel = models.get_model(app_label, model_name) class MadeLookupChannel(LookupChannel): model = themodel search_field = arg_search_field return MadeLookupChannel() django-ajax-selects-1.3.3/ajax_select/admin.py000066400000000000000000000014021224067352200212650ustar00rootroot00000000000000 from ajax_select.fields import autoselect_fields_check_can_add from django.contrib import admin class AjaxSelectAdmin(admin.ModelAdmin): """ in order to get + popup functions subclass this or do the same hook inside of your get_form """ def get_form(self, request, obj=None, **kwargs): form = super(AjaxSelectAdmin, self).get_form(request, obj, **kwargs) autoselect_fields_check_can_add(form, self.model, request.user) return form class AjaxSelectAdminTabularInline(admin.TabularInline): def get_formset(self, request, obj=None, **kwargs): fs = super(AjaxSelectAdminTabularInline, self).get_formset(request, obj, **kwargs) autoselect_fields_check_can_add(fs.form, self.model, request.user) return fs django-ajax-selects-1.3.3/ajax_select/fields.py000066400000000000000000000344601224067352200214550ustar00rootroot00000000000000 from ajax_select import get_lookup from django import forms from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse from django.forms.util import flatatt from django.template.loader import render_to_string from django.template.defaultfilters import force_escape from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from django.utils import simplejson as_default_help = u'Enter text to search.' def _media(self): # unless AJAX_SELECT_BOOTSTRAP == False # then load jquery and jquery ui + default css # where needed js = ('ajax_select/js/bootstrap.js', 'ajax_select/js/ajax_select.js') try: if not settings.AJAX_SELECT_BOOTSTRAP: js = ('ajax_select/js/ajax_select.js',) except AttributeError: pass return forms.Media(css={'all': ('ajax_select/css/ajax_select.css',)}, js=js) #################################################################################### class AutoCompleteSelectWidget(forms.widgets.TextInput): """ widget to select a model and return it as text """ media = property(_media) add_link = None def __init__(self, channel, help_text=u'', show_help_text=True, plugin_options={}, *args, **kwargs): self.plugin_options = plugin_options super(forms.widgets.TextInput, self).__init__(*args, **kwargs) self.channel = channel self.help_text = help_text self.show_help_text = show_help_text def render(self, name, value, attrs=None): value = value or '' final_attrs = self.build_attrs(attrs) self.html_id = final_attrs.pop('id', name) current_repr = '' initial = None lookup = get_lookup(self.channel) if value: objs = lookup.get_objects([value]) try: obj = objs[0] except IndexError: raise Exception("%s cannot find object:%s" % (lookup, value)) current_repr = lookup.format_item_display(obj) initial = [current_repr, obj.pk] if self.show_help_text: help_text = self.help_text else: help_text = u'' context = { 'name': name, 'html_id': self.html_id, 'current_id': value, 'current_repr': current_repr, 'help_text': help_text, 'extra_attrs': mark_safe(flatatt(final_attrs)), 'func_slug': self.html_id.replace("-", ""), 'add_link': self.add_link, } context.update(plugin_options(lookup, self.channel, self.plugin_options, initial)) return mark_safe(render_to_string(('autocompleteselect_%s.html' % self.channel, 'autocompleteselect.html'), context)) def value_from_datadict(self, data, files, name): got = data.get(name, None) if got: return long(got) else: return None def id_for_label(self, id_): return '%s_text' % id_ class AutoCompleteSelectField(forms.fields.CharField): """ form field to select a model for a ForeignKey db field """ channel = None def __init__(self, channel, *args, **kwargs): self.channel = channel widget = kwargs.get("widget", False) if not widget or not isinstance(widget, AutoCompleteSelectWidget): widget_kwargs = dict( channel=channel, help_text=kwargs.get('help_text', _(as_default_help)), show_help_text=kwargs.pop('show_help_text', True), plugin_options=kwargs.pop('plugin_options', {}) ) kwargs["widget"] = AutoCompleteSelectWidget(**widget_kwargs) super(AutoCompleteSelectField, self).__init__(max_length=255, *args, **kwargs) def clean(self, value): if value: lookup = get_lookup(self.channel) objs = lookup.get_objects([value]) if len(objs) != 1: # someone else might have deleted it while you were editing # or your channel is faulty # out of the scope of this field to do anything more than tell you it doesn't exist raise forms.ValidationError(u"%s cannot find object: %s" % (lookup, value)) return objs[0] else: if self.required: raise forms.ValidationError(self.error_messages['required']) return None def check_can_add(self, user, model): _check_can_add(self, user, model) #################################################################################### class AutoCompleteSelectMultipleWidget(forms.widgets.SelectMultiple): """ widget to select multiple models """ media = property(_media) add_link = None def __init__(self, channel, help_text='', show_help_text=True, plugin_options={}, *args, **kwargs): super(AutoCompleteSelectMultipleWidget, self).__init__(*args, **kwargs) self.channel = channel self.help_text = help_text self.show_help_text = show_help_text self.plugin_options = plugin_options def render(self, name, value, attrs=None): if value is None: value = [] final_attrs = self.build_attrs(attrs) self.html_id = final_attrs.pop('id', name) lookup = get_lookup(self.channel) # eg. value = [3002L, 1194L] if value: # |pk|pk| of current current_ids = "|" + "|".join(str(pk) for pk in value) + "|" else: current_ids = "|" objects = lookup.get_objects(value) # text repr of currently selected items initial = [] for obj in objects: display = lookup.format_item_display(obj) initial.append([display, obj.pk]) if self.show_help_text: help_text = self.help_text else: help_text = u'' context = { 'name': name, 'html_id': self.html_id, 'current': value, 'current_ids': current_ids, 'current_reprs': mark_safe(simplejson.dumps(initial)), 'help_text': help_text, 'extra_attrs': mark_safe(flatatt(final_attrs)), 'func_slug': self.html_id.replace("-", ""), 'add_link': self.add_link, } context.update(plugin_options(lookup, self.channel, self.plugin_options, initial)) return mark_safe(render_to_string(('autocompleteselectmultiple_%s.html' % self.channel, 'autocompleteselectmultiple.html'), context)) def value_from_datadict(self, data, files, name): # eg. u'members': [u'|229|4688|190|'] return [long(val) for val in data.get(name, '').split('|') if val] def id_for_label(self, id_): return '%s_text' % id_ class AutoCompleteSelectMultipleField(forms.fields.CharField): """ form field to select multiple models for a ManyToMany db field """ channel = None def __init__(self, channel, *args, **kwargs): self.channel = channel help_text = kwargs.get('help_text') show_help_text = kwargs.pop('show_help_text', False) if not (help_text is None): # '' will cause translation to fail # should be u'' if type(help_text) == str: help_text = unicode(help_text) # django admin appends "Hold down "Control",..." to the help text # regardless of which widget is used. so even when you specify an explicit help text it appends this other default text onto the end. # This monkey patches the help text to remove that if help_text != u'': if type(help_text) != unicode: # ideally this could check request.LANGUAGE_CODE translated = help_text.translate(settings.LANGUAGE_CODE) else: translated = help_text django_default_help = _(u'Hold down "Control", or "Command" on a Mac, to select more than one.').translate(settings.LANGUAGE_CODE) if django_default_help in translated: cleaned_help = translated.replace(django_default_help, '').strip() # probably will not show up in translations if cleaned_help: help_text = cleaned_help else: help_text = u"" show_help_text = False else: help_text = _(as_default_help) # django admin will also show help text outside of the display # area of the widget. this results in duplicated help. # it should just let the widget do the rendering # so by default do not show it in widget # if using in a normal form then set to True when creating the field widget_kwargs = { 'channel': channel, 'help_text': help_text, 'show_help_text': show_help_text, 'plugin_options': kwargs.pop('plugin_options', {}) } kwargs['widget'] = AutoCompleteSelectMultipleWidget(**widget_kwargs) kwargs['help_text'] = help_text super(AutoCompleteSelectMultipleField, self).__init__(*args, **kwargs) def clean(self, value): if not value and self.required: raise forms.ValidationError(self.error_messages['required']) return value # a list of IDs from widget value_from_datadict def check_can_add(self, user, model): _check_can_add(self, user, model) #################################################################################### class AutoCompleteWidget(forms.TextInput): """ Widget to select a search result and enter the result as raw text in the text input field. the user may also simply enter text and ignore any auto complete suggestions. """ media = property(_media) channel = None help_text = '' html_id = '' def __init__(self, channel, *args, **kwargs): self.channel = channel self.help_text = kwargs.pop('help_text', '') self.show_help_text = kwargs.pop('show_help_text', True) self.plugin_options = kwargs.pop('plugin_options', {}) super(AutoCompleteWidget, self).__init__(*args, **kwargs) def render(self, name, value, attrs=None): initial = value or '' final_attrs = self.build_attrs(attrs) self.html_id = final_attrs.pop('id', name) lookup = get_lookup(self.channel) if self.show_help_text: help_text = self.help_text else: help_text = u'' context = { 'current_repr': initial, 'current_id': initial, 'help_text': help_text, 'html_id': self.html_id, 'name': name, 'extra_attrs': mark_safe(flatatt(final_attrs)), 'func_slug': self.html_id.replace("-", ""), } context.update(plugin_options(lookup, self.channel, self.plugin_options, initial)) templates = ('autocomplete_%s.html' % self.channel, 'autocomplete.html') return mark_safe(render_to_string(templates, context)) class AutoCompleteField(forms.CharField): """ Field uses an AutoCompleteWidget to lookup possible completions using a channel and stores raw text (not a foreign key) """ channel = None def __init__(self, channel, *args, **kwargs): self.channel = channel widget_kwargs = dict( help_text=kwargs.get('help_text', _(as_default_help)), show_help_text=kwargs.pop('show_help_text', True), plugin_options=kwargs.pop('plugin_options', {}) ) if 'attrs' in kwargs: widget_kwargs['attrs'] = kwargs.pop('attrs') widget = AutoCompleteWidget(channel, **widget_kwargs) defaults = {'max_length': 255, 'widget': widget} defaults.update(kwargs) super(AutoCompleteField, self).__init__(*args, **defaults) #################################################################################### def _check_can_add(self, user, model): """ check if the user can add the model, deferring first to the channel if it implements can_add() else using django's default perm check. if it can add, then enable the widget to show the + link """ lookup = get_lookup(self.channel) if hasattr(lookup, 'can_add'): can_add = lookup.can_add(user, model) else: ctype = ContentType.objects.get_for_model(model) can_add = user.has_perm("%s.add_%s" % (ctype.app_label, ctype.model)) if can_add: self.widget.add_link = reverse('add_popup', kwargs={'app_label': model._meta.app_label, 'model': model._meta.object_name.lower()}) def autoselect_fields_check_can_add(form, model, user): """ check the form's fields for any autoselect fields and enable their widgets with + sign add links if permissions allow""" for name, form_field in form.declared_fields.iteritems(): if isinstance(form_field, (AutoCompleteSelectMultipleField, AutoCompleteSelectField)): db_field = model._meta.get_field_by_name(name)[0] form_field.check_can_add(user, db_field.rel.to) def plugin_options(channel, channel_name, widget_plugin_options, initial): """ Make a JSON dumped dict of all options for the jquery ui plugin itself """ po = {} if initial: po['initial'] = initial po.update(getattr(channel, 'plugin_options', {})) po.update(widget_plugin_options) if not po.get('min_length'): # backward compatibility: honor the channel's min_length attribute # will deprecate that some day and prefer to use plugin_options po['min_length'] = getattr(channel, 'min_length', 1) if not po.get('source'): po['source'] = reverse('ajax_lookup', kwargs={'channel': channel_name}) # allow html unless explictly false if po.get('html') is None: po['html'] = True return { 'plugin_options': mark_safe(simplejson.dumps(po)), 'data_plugin_options': force_escape(simplejson.dumps(po)), # continue to support any custom templates that still expect these 'lookup_url': po['source'], 'min_length': po['min_length'] } django-ajax-selects-1.3.3/ajax_select/models.py000066400000000000000000000000521224067352200214600ustar00rootroot00000000000000# blank file so django recognizes the app django-ajax-selects-1.3.3/ajax_select/static/000077500000000000000000000000001224067352200211155ustar00rootroot00000000000000django-ajax-selects-1.3.3/ajax_select/static/ajax_select/000077500000000000000000000000001224067352200233775ustar00rootroot00000000000000django-ajax-selects-1.3.3/ajax_select/static/ajax_select/css/000077500000000000000000000000001224067352200241675ustar00rootroot00000000000000django-ajax-selects-1.3.3/ajax_select/static/ajax_select/css/ajax_select.css000066400000000000000000000020161224067352200271620ustar00rootroot00000000000000.results_on_deck .ui-icon-trash { float: left; cursor: pointer; } .results_on_deck { padding: 0.25em 0; } form .aligned .results_on_deck { padding-left: 38px; margin-left: 7em; } .results_on_deck > div { margin-bottom: 0.5em; } .ui-autocomplete-loading { background: url('../images/loading-indicator.gif') no-repeat; background-origin: content-box; background-position: right; } ul.ui-autocomplete { /* this is the dropdown menu. if max-width is not set and you are using django-admin then the dropdown is the width of your whole page body (totally wrong). this sets max-width at 60% which is graceful at full page or in a popup or on a small width window. fixed width is harder see http://stackoverflow.com/questions/4607164/changing-width-of-jquery-ui-autocomplete-widgets-individually */ max-width: 60%; margin: 0; padding: 0; position: absolute; } ul.ui-autocomplete li { list-style-type: none; padding: 0; } ul.ui-autocomplete li a { display: block; padding: 2px 3px; cursor: pointer; } django-ajax-selects-1.3.3/ajax_select/static/ajax_select/images/000077500000000000000000000000001224067352200246445ustar00rootroot00000000000000django-ajax-selects-1.3.3/ajax_select/static/ajax_select/images/loading-indicator.gif000066400000000000000000000030211224067352200307160ustar00rootroot00000000000000GIF89aݻwwwfffUUUDDD333"""! NETSCAPE2.0!,w $B$B##( R!!,c $PxB +*-[dඁ+i@ )`L ?'I`JGb Ph XB)0׸XQ# } No "tI+ZI!!,\ $P`8* 1h0rx8BQaV  !MDl!4%BBe PDY00!!,] $$I>Q] d"28 GqH9 A2ȀB", DH('4C \0`UL"r(!! ,d $dI`ìkBB m A72, (PX鲪 8@R%a K*D2E {$ft5C%!;django-ajax-selects-1.3.3/ajax_select/static/ajax_select/js/000077500000000000000000000000001224067352200240135ustar00rootroot00000000000000django-ajax-selects-1.3.3/ajax_select/static/ajax_select/js/ajax_select.js000066400000000000000000000141741224067352200266420ustar00rootroot00000000000000'use strict'; (function ($) { $.fn.autocompleteselect = function (options) { return this.each(function () { var id = this.id, $this = $(this), $text = $('#' + id + '_text'), $deck = $('#' + id + '_on_deck'); function receiveResult(event, ui) { if ($this.val()) { kill(); } $this.val(ui.item.pk); $text.val(''); addKiller(ui.item.repr); $deck.trigger('added', [ui.item.pk, ui.item]); return false; } function addKiller(repr, pk) { var killer_id = 'kill_' + pk + id, killButton = 'X '; if (repr) { $deck.empty(); $deck.append('
' + killButton + repr + '
'); } else { $('#' + id+'_on_deck > div').prepend(killButton); } $('#' + killer_id).click(function () { kill(); $deck.trigger('killed', [pk]); }); } function kill() { $this.val(''); $deck.children().fadeOut(1.0).remove(); } options.select = receiveResult; $text.autocomplete(options); if (options.initial) { addKiller(options.initial[0], options.initial[1]); } $this.bind('didAddPopup', function (event, pk, repr) { receiveResult(null, {item: {pk: pk, repr: repr}}); }); }); }; $.fn.autocompleteselectmultiple = function (options) { return this.each(function () { var id = this.id, $this = $(this), $text = $('#' + id+'_text'), $deck = $('#' + id+'_on_deck'); function receiveResult(event, ui) { var pk = ui.item.pk, prev = $this.val(); if (prev.indexOf('|'+pk+'|') === -1) { $this.val((prev ? prev : '|') + pk + '|'); addKiller(ui.item.repr, pk); $text.val(''); $deck.trigger('added', [ui.item.pk, ui.item]); } return false; } function addKiller(repr, pk) { var killer_id = 'kill_' + pk + id, killButton = 'X '; $deck.append('
' + killButton + repr + '
'); $('#' + killer_id).click(function () { kill(pk); $deck.trigger('killed', [pk]); }); } function kill(pk) { $this.val($this.val().replace('|' + pk + '|', '|')); $('#' + id+'_on_deck_'+pk).fadeOut().remove(); } options.select = receiveResult; $text.autocomplete(options); if (options.initial) { $.each(options.initial, function (i, its) { addKiller(its[0], its[1]); }); } $this.bind('didAddPopup', function (event, pk, repr) { receiveResult(null, {item: {pk: pk, repr: repr }}); }); }); }; function addAutoComplete (inp, callback) { var $inp = $(inp), html_id = inp.id, prefix_id = html_id, opts = JSON.parse($inp.attr('data-plugin-options')), prefix = 0; /* detects inline forms and converts the html_id if needed */ if (html_id.indexOf('__prefix__') !== -1) { // Some dirty loop to find the appropriate element to apply the callback to while ($('#' + html_id).length) { html_id = prefix_id.replace(/__prefix__/, prefix++); } html_id = prefix_id.replace(/__prefix__/, prefix - 2); // Ignore the first call to this function, the one that is triggered when // page is loaded just because the 'empty' form is there. if ($('#' + html_id + ', #' + html_id + '_text').hasClass('ui-autocomplete-input')) { return; } } callback($inp, opts); } // allow html in the results menu // https://github.com/scottgonzalez/jquery-ui-extensions var proto = $.ui.autocomplete.prototype, initSource = proto._initSource; function filter(array, term) { var matcher = new RegExp($.ui.autocomplete.escapeRegex(term), 'i'); return $.grep(array, function(value) { return matcher.test($('
').html(value.label || value.value || value).text()); }); } $.extend(proto, { _initSource: function() { if (this.options.html && $.isArray(this.options.source)) { this.source = function(request, response) { response(filter(this.options.source, request.term)); }; } else { initSource.call(this); } }, _renderItem: function(ul, item) { var body = this.options.html ? item.repr : item.label; return $('
  • ') .data('item.autocomplete', item) .append($('')[this.options.html ? 'html' : 'text' ](body)) .appendTo(ul); } }); /* the popup handler requires RelatedObjects.js which is part of the django admin js so if using outside of the admin then you would need to include that manually */ window.didAddPopup = function (win, newId, newRepr) { var name = window.windowname_to_id(win.name); $('#' + name).trigger('didAddPopup', [window.html_unescape(newId), window.html_unescape(newRepr)]); win.close(); }; // activate any on page $(window).bind('init-autocomplete', function () { $('input[data-ajax-select=autocomplete]').each(function (i, inp) { addAutoComplete(inp, function ($inp, opts) { opts.select = function (event, ui) { $inp.val(ui.item.value).trigger('added', [ui.item.pk, ui.item]); return false; }; $inp.autocomplete(opts); }); }); $('input[data-ajax-select=autocompleteselect]').each(function (i, inp) { addAutoComplete(inp, function ($inp, opts) { $inp.autocompleteselect(opts); }); }); $('input[data-ajax-select=autocompleteselectmultiple]').each(function (i, inp) { addAutoComplete(inp, function ($inp, opts) { $inp.autocompleteselectmultiple(opts); }); }); }); $(document).ready(function () { // if dynamically injecting forms onto a page // you can trigger them to be ajax-selects-ified: $(window).trigger('init-autocomplete'); }); })(window.jQuery); django-ajax-selects-1.3.3/ajax_select/static/ajax_select/js/bootstrap.js000066400000000000000000000013711224067352200263700ustar00rootroot00000000000000// load jquery and jquery-ui if needed // into window.jQuery if (typeof window.jQuery === 'undefined') { document.write('