From 6770e661b6f30fda6337abee4d6905c8cd55a338 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:07:44 +0200 Subject: [PATCH 01/40] move settings, views, urls, models into a subfolder as per currently recommended django project structure --- example/example/__init__.py | 0 example/{ => example}/admin.py | 0 example/{ => example}/forms.py | 0 example/{ => example}/lookups.py | 0 example/{ => example}/models.py | 0 example/{ => example}/settings.py | 5 +++-- example/{ => example}/urls.py | 2 +- example/{ => example}/views.py | 0 8 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 example/example/__init__.py rename example/{ => example}/admin.py (100%) rename example/{ => example}/forms.py (100%) rename example/{ => example}/lookups.py (100%) rename example/{ => example}/models.py (100%) rename example/{ => example}/settings.py (96%) rename example/{ => example}/urls.py (77%) rename example/{ => example}/views.py (100%) diff --git a/example/example/__init__.py b/example/example/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/example/admin.py b/example/example/admin.py similarity index 100% rename from example/admin.py rename to example/example/admin.py diff --git a/example/forms.py b/example/example/forms.py similarity index 100% rename from example/forms.py rename to example/example/forms.py diff --git a/example/lookups.py b/example/example/lookups.py similarity index 100% rename from example/lookups.py rename to example/example/lookups.py diff --git a/example/models.py b/example/example/models.py similarity index 100% rename from example/models.py rename to example/example/models.py diff --git a/example/settings.py b/example/example/settings.py similarity index 96% rename from example/settings.py rename to example/example/settings.py index 8b2de5c6f6..49a689dab4 100644 --- a/example/settings.py +++ b/example/example/settings.py @@ -8,10 +8,11 @@ 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.admin', + 'django.contrib.staticfiles', 'example', #################################### - 'ajax_select', # <- add the app + 'ajax_select', # <- add the app #################################### ) @@ -54,7 +55,7 @@ # but otherwise harmless # False/None: [default] # does not inline anything. include the css/js files in your compressor stack -# or include them in the head of the admin/base_site.html template +# or include them in the head of the admin/base_example.html template # this is the most efficient but takes the longest to configure # when using staticfiles you may implement your own ajax_select.css and customize to taste diff --git a/example/urls.py b/example/example/urls.py similarity index 77% rename from example/urls.py rename to example/example/urls.py index 765def8b4c..9a66e31082 100644 --- a/example/urls.py +++ b/example/example/urls.py @@ -6,7 +6,7 @@ admin.autodiscover() urlpatterns = patterns('', - url(r'^search_form', view='views.search_form',name='search_form'), + url(r'^search_form', view='example.views.search_form',name='search_form'), (r'^admin/lookups/', include(ajax_select_urls)), (r'^admin/', include(admin.site.urls)), ) diff --git a/example/views.py b/example/example/views.py similarity index 100% rename from example/views.py rename to example/example/views.py From 1343bc38b8dd02f8e4f3e2aee4bebdf692568f7c Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:08:19 +0200 Subject: [PATCH 02/40] install script: allow choosing which django to install by command line arg --- example/install.sh | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/example/install.sh b/example/install.sh index 3ed42d344a..694e0df9fd 100755 --- a/example/install.sh +++ b/example/install.sh @@ -1,17 +1,30 @@ +#!/bin/sh -# creates a virtualenv and installs a django here +# break on any error +set -e + +# creates a virtualenv virtualenv AJAXSELECTS source AJAXSELECTS/bin/activate -pip install django -# put ajax selects in the path +DJANGO=$1 +if [ "$DJANGO" != "" ]; then + echo "Installing Django $DJANGO" + pip install Django==$DJANGO +else + echo "Installing latest django" + pip install django +fi + +echo "Symlinking ajax_select into this app directory" ln -s ../ajax_select/ ./ajax_select -# create sqllite database +echo "Creating a sqllite database" ./manage.py syncdb -echo "type 'source AJAXSELECTS/bin/activate' to activate the virtualenv" -echo "then run: ./manage.py runserver" +echo "run: ./manage.py runserver" echo "and visit http://127.0.0.1:8000/admin/" echo "type 'deactivate' to close the virtualenv or just close the shell" +echo "type 'source AJAXSELECTS/bin/activate' to re-activate the virtualenv" +exit 0 From e2e186eb222858d577952fe99159db43cccd89b1 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:11:04 +0200 Subject: [PATCH 03/40] add a Makefile --- example/Makefile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 example/Makefile diff --git a/example/Makefile b/example/Makefile new file mode 100644 index 0000000000..0d2eb15541 --- /dev/null +++ b/example/Makefile @@ -0,0 +1,25 @@ + + +install: + $(shell ./install.sh $(DJANGO) 1>&2) + @echo + @echo NOW DO: + @echo source AJAXSELECTS/bin/active + @echo ./manage.py runserver + @echo browse: http://localhost:8080/admin/ + +clean: + @echo Deleting virtualenv + rm -rf AJAXSELECTS + +cleandb: + @echo Removing sqllite db + rm ajax_selects_example + +help: + @echo make install + @echo or: + @echo make clean install DJANGO=1.4.2 + + +.PHONY: install clean cleandb help From c7ff8afa5caf59a92636a5bd0fa6b90a4d92f676 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:11:23 +0200 Subject: [PATCH 04/40] update manage.py for django 1.4 --- example/manage-1.3.x.py | 11 +++++++++++ example/manage.py | 14 ++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 example/manage-1.3.x.py diff --git a/example/manage-1.3.x.py b/example/manage-1.3.x.py new file mode 100644 index 0000000000..5e78ea979e --- /dev/null +++ b/example/manage-1.3.x.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + 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__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/example/manage.py b/example/manage.py index 5e78ea979e..0b3e3d7503 100755 --- a/example/manage.py +++ b/example/manage.py @@ -1,11 +1,9 @@ #!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - 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__) - sys.exit(1) +import os, sys if __name__ == "__main__": - execute_manager(settings) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) From e5722d9404b8188faf853d4e97fdf5b5379b6efa Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:15:24 +0200 Subject: [PATCH 05/40] fix: move staticfiles into static/ajax_select to work correctly with staticfiles Fixes #6 Fixes #32 --- ajax_select/fields.py | 4 ++-- .../static/{ => ajax_select}/css/ajax_select.css | 0 .../{ => ajax_select}/images/loading-indicator.gif | Bin .../static/{ => ajax_select}/js/ajax_select.js | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename ajax_select/static/{ => ajax_select}/css/ajax_select.css (100%) rename ajax_select/static/{ => ajax_select}/images/loading-indicator.gif (100%) rename ajax_select/static/{ => ajax_select}/js/ajax_select.js (100%) diff --git a/ajax_select/fields.py b/ajax_select/fields.py index 5863c647cf..03c2a42ecb 100644 --- a/ajax_select/fields.py +++ b/ajax_select/fields.py @@ -395,9 +395,9 @@ def bootstrap(): b['inline'] = '' if inlines == 'inline': directory = os.path.dirname( os.path.realpath(__file__) ) - f = open(os.path.join(directory,"static","css","ajax_select.css")) + f = open(os.path.join(directory,"static","ajax_select","css","ajax_select.css")) css = f.read() - f = open(os.path.join(directory,"static","js","ajax_select.js")) + f = open(os.path.join(directory,"static","ajax_select","js","ajax_select.js")) js = f.read() b['inline'] = mark_safe(u"""""" % (css,js)) elif inlines == 'staticfiles': diff --git a/ajax_select/static/css/ajax_select.css b/ajax_select/static/ajax_select/css/ajax_select.css similarity index 100% rename from ajax_select/static/css/ajax_select.css rename to ajax_select/static/ajax_select/css/ajax_select.css diff --git a/ajax_select/static/images/loading-indicator.gif b/ajax_select/static/ajax_select/images/loading-indicator.gif similarity index 100% rename from ajax_select/static/images/loading-indicator.gif rename to ajax_select/static/ajax_select/images/loading-indicator.gif diff --git a/ajax_select/static/js/ajax_select.js b/ajax_select/static/ajax_select/js/ajax_select.js similarity index 100% rename from ajax_select/static/js/ajax_select.js rename to ajax_select/static/ajax_select/js/ajax_select.js From ffb608c2811f6c918d47cc86e0669166cd2ff8d7 Mon Sep 17 00:00:00 2001 From: artscoop Date: Sat, 23 Mar 2013 05:01:49 +0100 Subject: [PATCH 06/40] Update urls.py for Django 1.6 compat Compatibility with Django 1.5 and 1.6 --- ajax_select/urls.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ajax_select/urls.py b/ajax_select/urls.py index 42e9ca8830..59c459ed13 100644 --- a/ajax_select/urls.py +++ b/ajax_select/urls.py @@ -1,5 +1,7 @@ - -from django.conf.urls.defaults import * +try: + from django.conf.urls import * +except: + from django.conf.urls.defaults import * urlpatterns = patterns('', From ac21a9abe0dd22ef3a3d1b24f4ba5e9aacd4eca1 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:20:40 +0200 Subject: [PATCH 07/40] example app: remove deprec URLField verify_exists --- example/example/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/example/models.py b/example/example/models.py index 4bc0714eb4..5c2f4874ce 100644 --- a/example/example/models.py +++ b/example/example/models.py @@ -19,7 +19,7 @@ class Group(models.Model): name = models.CharField(max_length=200,unique=True) members = models.ManyToManyField(Person,blank=True,help_text="Enter text to search for and add each member of the group.") - url = models.URLField(blank=True, verify_exists=False) + url = models.URLField(blank=True) def __unicode__(self): return self.name @@ -31,7 +31,7 @@ class Label(models.Model): name = models.CharField(max_length=200,unique=True) owner = models.ForeignKey(Person,blank=True,null=True) - url = models.URLField(blank=True, verify_exists=False) + url = models.URLField(blank=True) def __unicode__(self): return self.name From 1e139fbdf731c0a461ca57b96648acb35784d398 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:44:42 +0200 Subject: [PATCH 08/40] Fixes #39 although I cannot replicate it on django 1.5.0 or 1.5.3 --- ajax_select/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ajax_select/views.py b/ajax_select/views.py index e886e15ec2..2f68ca51b3 100644 --- a/ajax_select/views.py +++ b/ajax_select/views.py @@ -62,7 +62,7 @@ def add_popup(request,app_label,model): response = admin.add_view(request,request.path) if request.method == 'POST': - if 'opener.dismissAddAnotherPopup' in response.content: return HttpResponse( response.content.replace('dismissAddAnotherPopup','didAddPopup' ) ) + if 'opener.dismissAddAnotherPopup' in unicode(response.content): return response From a9149af56122047ff4e6a76e4856ccab0834b5f4 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 1 Apr 2013 20:45:51 +0200 Subject: [PATCH 09/40] cleanup for PEP8 --- ajax_select/views.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ajax_select/views.py b/ajax_select/views.py index 2f68ca51b3..dc9e6daddc 100644 --- a/ajax_select/views.py +++ b/ajax_select/views.py @@ -6,7 +6,7 @@ from django.utils import simplejson -def ajax_lookup(request,channel): +def ajax_lookup(request, channel): """ this view supplies results for foreign keys and many to many fields """ @@ -19,23 +19,23 @@ def ajax_lookup(request,channel): query = request.GET['term'] else: if 'term' not in request.POST: - return HttpResponse('') # suspicious + return HttpResponse('') # suspicious query = request.POST['term'] lookup = get_lookup(channel) - if hasattr(lookup,'check_auth'): + if hasattr(lookup, 'check_auth'): lookup.check_auth(request) if len(query) >= getattr(lookup, 'min_length', 1): - instances = lookup.get_query(query,request) + instances = lookup.get_query(query, request) else: instances = [] results = simplejson.dumps([ { - 'pk': unicode(getattr(item,'pk',None)), + 'pk': unicode(getattr(item, 'pk', None)), 'value': lookup.get_result(item), - 'match' : lookup.format_match(item), + 'match': lookup.format_match(item), 'repr': lookup.format_item_display(item) } for item in instances ]) @@ -43,7 +43,7 @@ def ajax_lookup(request,channel): return HttpResponse(results, mimetype='application/javascript') -def add_popup(request,app_label,model): +def add_popup(request, app_label, model): """ this presents the admin site popup add view (when you click the green +) make sure that you have added ajax_select.urls to your urls.py: @@ -60,9 +60,8 @@ def add_popup(request,app_label,model): # TODO : should detect where we really are admin.admin_site.root_path = "/ajax_select/" - response = admin.add_view(request,request.path) + response = admin.add_view(request, request.path) if request.method == 'POST': - return HttpResponse( response.content.replace('dismissAddAnotherPopup','didAddPopup' ) ) if 'opener.dismissAddAnotherPopup' in unicode(response.content): + return HttpResponse(response.content.replace('dismissAddAnotherPopup', 'didAddPopup')) return response - From b1847623fd3d12a2ba8f21fd0b2f08227706a11d Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 2 Apr 2013 00:51:34 +0200 Subject: [PATCH 10/40] add Media to widgets; add a bootstrap.js to load jquery and jquery-ui where needed --- ajax_select/fields.py | 23 +++++++++++++++++-- .../static/ajax_select/js/bootstrap.js | 13 +++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 ajax_select/static/ajax_select/js/bootstrap.js diff --git a/ajax_select/fields.py b/ajax_select/fields.py index 03c2a42ecb..25c0de1d6d 100644 --- a/ajax_select/fields.py +++ b/ajax_select/fields.py @@ -5,23 +5,36 @@ from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse from django.forms.util import flatatt -from django.template.defaultfilters import escapejs from django.template.loader import render_to_string from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ -from django.conf import settings from django.utils import simplejson import os 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 settings.AJAX_SELECT_BOOTSTRAP == False: + 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, @@ -133,6 +146,8 @@ class AutoCompleteSelectMultipleWidget(forms.widgets.SelectMultiple): """ widget to select multiple models """ + media = property(_media) + add_link = None def __init__(self, @@ -269,10 +284,14 @@ def 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 = '' diff --git a/ajax_select/static/ajax_select/js/bootstrap.js b/ajax_select/static/ajax_select/js/bootstrap.js new file mode 100644 index 0000000000..e474405e85 --- /dev/null +++ b/ajax_select/static/ajax_select/js/bootstrap.js @@ -0,0 +1,13 @@ +// load jquery and jquery-ui if needed +// into window.jQuery +if (typeof jQuery === 'undefined') { + try { // use django admins + jQuery=django.jQuery; + } catch(err) { + document.write('""" % (css,js)) - elif inlines == 'staticfiles': - b['inline'] = mark_safe("""""" % (settings.STATIC_URL,settings.STATIC_URL)) - - return b - - diff --git a/ajax_select/templates/ajax_select/bootstrap.html b/ajax_select/templates/ajax_select/bootstrap.html deleted file mode 100644 index 6ad228472a..0000000000 --- a/ajax_select/templates/ajax_select/bootstrap.html +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/ajax_select/templates/autocomplete.html b/ajax_select/templates/autocomplete.html index fac7aa6d92..be093ced82 100644 --- a/ajax_select/templates/autocomplete.html +++ b/ajax_select/templates/autocomplete.html @@ -1,4 +1,3 @@ -{% if bootstrap %}{% include "ajax_select/bootstrap.html" %}{% endif %} {% block help %}{% if help_text %}

{{ help_text }}

{% endif %}{% endblock %} -{{ inline }} diff --git a/ajax_select/templates/autocompleteselect.html b/ajax_select/templates/autocompleteselect.html index 83c99dabc0..94e2ee5851 100644 --- a/ajax_select/templates/autocompleteselect.html +++ b/ajax_select/templates/autocompleteselect.html @@ -1,4 +1,3 @@ -{% if bootstrap %}{% include "ajax_select/bootstrap.html" %}{% endif %} {% if add_link %} @@ -16,4 +15,3 @@ {% block help %}{% if help_text %}

{{help_text}}

{% endif %}{% endblock %}
-{{ inline }} diff --git a/ajax_select/templates/autocompleteselectmultiple.html b/ajax_select/templates/autocompleteselectmultiple.html index 6f0aaa7f35..14a758b3b2 100644 --- a/ajax_select/templates/autocompleteselectmultiple.html +++ b/ajax_select/templates/autocompleteselectmultiple.html @@ -1,4 +1,3 @@ -{% if bootstrap %}{% include "ajax_select/bootstrap.html" %}{% endif %} {% if add_link %} add @@ -16,4 +15,3 @@ {# django admin adds the help text. this is for use outside of the admin #} {% block help %}{% if help_text %}

{{help_text}}

{% endif %}{% endblock %} -{{ inline }} From 4b582eda7374ec7433b9c1989d9f8e8d5647f5a5 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 2 Apr 2013 00:58:48 +0200 Subject: [PATCH 12/40] update example app to use static files and form media --- example/example/admin.py | 4 ++-- example/example/settings.py | 2 +- example/example/urls.py | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/example/example/admin.py b/example/example/admin.py index 01ea3ea652..7d64a91519 100644 --- a/example/example/admin.py +++ b/example/example/admin.py @@ -7,7 +7,7 @@ -class PersonAdmin(admin.ModelAdmin): +class PersonAdmin(AjaxSelectAdmin): pass @@ -75,7 +75,7 @@ class BookInline(admin.TabularInline): # autoselect_fields_check_can_add(fs.form,self.model,request.user) # return fs -class AuthorAdmin(admin.ModelAdmin): +class AuthorAdmin(AjaxSelectAdmin): inlines = [ BookInline, ] diff --git a/example/example/settings.py b/example/example/settings.py index 49a689dab4..e74f2b8342 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -123,7 +123,7 @@ # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -STATIC_URL = '/media/' +STATIC_URL = '/static/' # Make this unique, and don't share it with nobody. SECRET_KEY = '=9fhrrwrazha6r_m)r#+in*@n@i322ubzy4r+zz%wz$+y(=qpb' diff --git a/example/example/urls.py b/example/example/urls.py index 9a66e31082..d137a639a6 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,12 +1,15 @@ -from django.conf.urls.defaults import * - +from django.conf.urls.defaults import patterns, include, url +from django.conf.urls.static import static from django.contrib import admin +from django.conf import settings from ajax_select import urls as ajax_select_urls + admin.autodiscover() urlpatterns = patterns('', url(r'^search_form', view='example.views.search_form',name='search_form'), (r'^admin/lookups/', include(ajax_select_urls)), (r'^admin/', include(admin.site.urls)), -) +) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + From 367d835b6a1b61415abf2b83bfbfb6a7b60010da Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 2 Apr 2013 01:03:26 +0200 Subject: [PATCH 13/40] update example settings documentation --- example/example/settings.py | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/example/example/settings.py b/example/example/settings.py index e74f2b8342..9a73db10ce 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -30,36 +30,18 @@ 'person' : ('example.lookups', 'PersonLookup'), 'group' : ('example.lookups', 'GroupLookup'), 'song' : ('example.lookups', 'SongLookup'), - 'cliche' : ('example.lookups','ClicheLookup') + 'cliche' : ('example.lookups', 'ClicheLookup') } -AJAX_SELECT_BOOTSTRAP = True -# True: [easiest] -# use the admin's jQuery if present else load from jquery's CDN -# use jqueryUI if present else load from jquery's CDN -# use jqueryUI theme if present else load one from jquery's CDN -# False/None/Not set: [default] -# you should include jQuery, jqueryUI + theme in your template - - -AJAX_SELECT_INLINES = 'inline' -# 'inline': [easiest] -# includes the js and css inline -# this gets you up and running easily -# but on large admin pages or with higher traffic it will be a bit wasteful. -# 'staticfiles': -# @import the css/js from {{STATIC_URL}}/ajax_selects using django's staticfiles app -# requires staticfiles to be installed and to run its management command to collect files -# this still includes the css/js multiple times and is thus inefficient -# but otherwise harmless -# False/None: [default] -# does not inline anything. include the css/js files in your compressor stack -# or include them in the head of the admin/base_example.html template -# this is the most efficient but takes the longest to configure - -# when using staticfiles you may implement your own ajax_select.css and customize to taste +# By default will use window.jQuery +# or Django Admin's jQuery +# or load one from google ajax apis +# then load jquery-ui and a default css +# Set this to False if for some reason you want to supply your own +# window.jQuery and jQuery UI +# AJAX_SELECT_BOOTSTRAP = False ########################################################################### From 48cb85351d85748cfbe5ee7d7153ecc9544018bb Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 2 Apr 2013 01:43:42 +0200 Subject: [PATCH 14/40] update README for form media / staticfiles integration --- README.md | 79 ++++++++++++++++++++++--------------------------------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 47166f7078..3c6f75cf7a 100644 --- a/README.md +++ b/README.md @@ -60,17 +60,18 @@ 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 = { - # pass a dict with the model and the field to search against - 'person' : {'model':'example.person', 'search_field':'name'} + # simple: search Person.objects.filter(name__icontains=q) + 'person' : {'model':'example.person', 'search_field':'name'}, + # define a custom lookup channel + 'song' : ('example.lookups', 'SongLookup') } - # magically include jqueryUI/js/css - AJAX_SELECT_BOOTSTRAP = True - AJAX_SELECT_INLINES = 'inline' + In your urls.py: @@ -98,30 +99,37 @@ In your admin.py: pass admin.site.register(Person,PersonAdmin) - # subclass AjaxSelectAdmin - class LabelAdmin(AjaxSelectAdmin): + 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') -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. NOT SO QUICK INSTALLATION ========================= Things that can be customized: -+ how and from where jQuery, jQueryUI, jQueryUI theme are loaded -+ whether to include js/css inline or for better performance via staticfiles or django-compress etc. + 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) -+ customizing the CSS -+ each channel could define its own template to change display or add extra javascript -+ custom javascript can respond to jQuery triggers when items are selected or removed ++ 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 @@ -171,43 +179,25 @@ Defines the available lookup channels. #### AJAX_SELECT_BOOTSTRAP -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. - -+ True: [easiest] - use jQuery if already present, else use the admin's jQuery else load from google's CDN - use jqueryUI if present else load from google's CDN - use jqueryUI theme if present else load one from google's CDN - -+ False/None/Not set: [default] - you should then include jQuery, jqueryUI + theme in your template or js compressor stack +By default it will include bootstrap.js in the widget media in order to locate a jQuery and a jQuery-UI +First one wins: -#### AJAX_SELECT_INLINES +* window.jQuery - if you included jQuery on the page +* django.jQuery - if you are on an admin page +* load from ajax.googleapis com CDN -This controls if and how these: +Likewise for jQuery-UI: - ajax_select/static/js/ajax_select.js - ajax_select/static/css/ajax_select.css +* window.jQuery.ui +* load from ajax.googleapis.com with default smoothness theme -are included inline in the html with each form field. +If you want your own theme then load it afterwards to override or: -+ 'inline': [easiest] - Includes the js and css inline - This gets you up and running easily and is fine for small sites. - But with many form fields this will be less efficient. + AJAX_SELECT_BOOTSTRAP = False -+ 'staticfiles': - @import the css/js from {{STATIC_URL}}/ajax_selects using `django.contrib.staticfiles` - Requires staticfiles to be installed and to run its management command to collect files. - This still imports the css/js multiple times and is thus inefficient but otherwise harmless. +and load your own jquery, jquery ui and theme. - When using staticfiles you may implement your own `ajax_select.css` and customize to taste as long - as your app is before ajax_select in the INSTALLED_APPS. - -+ False/None: [default] - Does not inline anything. You should include the css/js files in your compressor stack - or include them in the head of the admin/base_site.html template. - This is the most efficient but takes the longest to configure. urls.py @@ -488,11 +478,6 @@ If you are doing your own compress stack then of course you can include whatever 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 CSS refers to one image that is served from github (as a CDN): -!['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' - -Your own site's CSS could redefine that with a stronger declaration to point to whatever you like. - The trashcan icon comes from the jQueryUI theme by the css classes: "ui-icon ui-icon-trash" From 075559433c4895c4e258f62e78c962dd87b7735a Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 2 Apr 2013 01:45:41 +0200 Subject: [PATCH 15/40] Use local staticfiles hosted loading-indicator.gif Fixes #24 --- ajax_select/static/ajax_select/css/ajax_select.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ajax_select/static/ajax_select/css/ajax_select.css b/ajax_select/static/ajax_select/css/ajax_select.css index 233796dd23..86db25f0d6 100644 --- a/ajax_select/static/ajax_select/css/ajax_select.css +++ b/ajax_select/static/ajax_select/css/ajax_select.css @@ -13,7 +13,7 @@ form .aligned .results_on_deck { margin-bottom: 0.5em; } .ui-autocomplete-loading { - background: url('https://github.com/crucialfelix/django-ajax-selects/raw/master/ajax_select/static/images/loading-indicator.gif') no-repeat; + background: url('../images/loading-indicator.gif') no-repeat; background-origin: content-box; background-position: right; } From 483e48862ad23942315548cf828f45a3319c542f Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 2 Apr 2013 02:58:54 +0200 Subject: [PATCH 16/40] bumping default jquery to 1.9.1, jquery-ui to 1.8.24 jquery-ui versions after that break --- ajax_select/static/ajax_select/js/bootstrap.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ajax_select/static/ajax_select/js/bootstrap.js b/ajax_select/static/ajax_select/js/bootstrap.js index e474405e85..343ce89c9a 100644 --- a/ajax_select/static/ajax_select/js/bootstrap.js +++ b/ajax_select/static/ajax_select/js/bootstrap.js @@ -4,10 +4,10 @@ if (typeof jQuery === 'undefined') { try { // use django admins jQuery=django.jQuery; } catch(err) { - document.write(' {% block help %}{% if help_text %}

{{ help_text }}

{% endif %}{% endblock %} diff --git a/ajax_select/templates/autocompleteselect.html b/ajax_select/templates/autocompleteselect.html index b7a00e8fcf..77b67b01bc 100644 --- a/ajax_select/templates/autocompleteselect.html +++ b/ajax_select/templates/autocompleteselect.html @@ -3,15 +3,8 @@ {% if add_link %} add {% endif %} - +
{{current_repr|safe}}
- {% block help %}{% if help_text %}

{{help_text}}

{% endif %}{% endblock %} diff --git a/ajax_select/templates/autocompleteselectmultiple.html b/ajax_select/templates/autocompleteselectmultiple.html index b0e0a51b92..19eeca2888 100644 --- a/ajax_select/templates/autocompleteselectmultiple.html +++ b/ajax_select/templates/autocompleteselectmultiple.html @@ -2,16 +2,8 @@ {% if add_link %} add {% endif %} - +
- {# django admin adds the help text. this is for use outside of the admin #} {% block help %}{% if help_text %}

{{help_text}}

{% endif %}{% endblock %} From e58cfb934dc5ffa9be78297e9929c271acc69483 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sat, 5 Oct 2013 19:21:19 +0200 Subject: [PATCH 27/40] example: enable the + add option (now working) --- example/example/admin.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/example/example/admin.py b/example/example/admin.py index 4b4593745d..dd6efbf9f3 100644 --- a/example/example/admin.py +++ b/example/example/admin.py @@ -65,13 +65,12 @@ class BookInline(admin.TabularInline): show_help_text=True) extra = 2 - # + check add still not working - # no + appearing - # def get_formset(self, request, obj=None, **kwargs): - # from ajax_select.fields import autoselect_fields_check_can_add - # fs = super(BookInline, self).get_formset(request, obj,**kwargs) - # autoselect_fields_check_can_add(fs.form, self.model, request.user) - # return fs + # this enables the + add option + def get_formset(self, request, obj=None, **kwargs): + from ajax_select.fields import autoselect_fields_check_can_add + fs = super(BookInline, self).get_formset(request, obj,**kwargs) + autoselect_fields_check_can_add(fs.form, self.model, request.user) + return fs class AuthorAdmin(AjaxSelectAdmin): From 97bf774d54a1c676489a610df46db79298c4c5cf Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sat, 5 Oct 2013 19:28:34 +0200 Subject: [PATCH 28/40] add AdminSelectAdminTabularInline for easily adding admin + popup to inlines --- ajax_select/admin.py | 8 ++++++++ example/example/admin.py | 20 ++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ajax_select/admin.py b/ajax_select/admin.py index 61eaafcc7e..715435dfcf 100644 --- a/ajax_select/admin.py +++ b/ajax_select/admin.py @@ -13,3 +13,11 @@ def get_form(self, request, obj=None, **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 diff --git a/example/example/admin.py b/example/example/admin.py index dd6efbf9f3..2c8f30da5a 100644 --- a/example/example/admin.py +++ b/example/example/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from ajax_select import make_ajax_form -from ajax_select.admin import AjaxSelectAdmin +from ajax_select.admin import AjaxSelectAdmin, AjaxSelectAdminTabularInline from example.forms import ReleaseForm from example.models import * @@ -55,7 +55,9 @@ class ReleaseAdmin(AjaxSelectAdmin): admin.site.register(Release, ReleaseAdmin) -class BookInline(admin.TabularInline): +class BookInline(AjaxSelectAdminTabularInline): + + # AjaxSelectAdminTabularInline enables the + add option model = Book form = make_ajax_form(Book, { @@ -65,12 +67,14 @@ class BookInline(admin.TabularInline): show_help_text=True) extra = 2 - # this enables the + add option - def get_formset(self, request, obj=None, **kwargs): - from ajax_select.fields import autoselect_fields_check_can_add - fs = super(BookInline, self).get_formset(request, obj,**kwargs) - autoselect_fields_check_can_add(fs.form, self.model, request.user) - return fs + # to enable the + add option + # instead of your inline inheriting from AjaxSelectAdminTabularInline + # you could implement this + # def get_formset(self, request, obj=None, **kwargs): + # from ajax_select.fields import autoselect_fields_check_can_add + # fs = super(BookInline, self).get_formset(request, obj,**kwargs) + # autoselect_fields_check_can_add(fs.form, self.model, request.user) + # return fs class AuthorAdmin(AjaxSelectAdmin): From 3f3a8f01699ad9ddc53ec5acaffb6b6a30415a70 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sat, 5 Oct 2013 19:29:04 +0200 Subject: [PATCH 29/40] some example app docstrings --- example/example/models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/example/example/models.py b/example/example/models.py index 8c9f58918b..6c5981ed60 100644 --- a/example/example/models.py +++ b/example/example/models.py @@ -65,11 +65,24 @@ def __unicode__(self): class Author(models.Model): + """ Author has multiple books, + via foreign keys + """ + name = models.CharField(max_length=100) + def __unicode__(self): + return self.name + class Book(models.Model): + + """ Book has no admin, its an inline in the Author admin""" + author = models.ForeignKey(Author) title = models.CharField(max_length=100) about_group = models.ForeignKey(Group) mentions_persons = models.ManyToManyField(Person) + + def __unicode__(self): + return self.title From 819d9192e6ddc352ecfd7e828f79395ab2bf8105 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sat, 5 Oct 2013 19:29:28 +0200 Subject: [PATCH 30/40] add github @links --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c14f7d381c..a1cd6b499e 100644 --- a/README.md +++ b/README.md @@ -537,7 +537,7 @@ 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), Brian May +In particular thanks for help in the 1.2 version: @sjrd (Sébastien Doeraene), @brianmay License From 0b929b6998a8b61cd3f354e97d70f822646dc69e Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sat, 5 Oct 2013 19:53:30 +0200 Subject: [PATCH 31/40] minor docs cleanup --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a1cd6b499e..c3807ddae4 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ In settings.py : # 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'}, + 'person' : {'model': 'example.person', 'search_field': 'name'}, # define a custom lookup channel 'song' : ('example.lookups', 'SongLookup') } @@ -173,7 +173,9 @@ Defines the available lookup channels. #### AJAX_SELECT_BOOTSTRAP -By default it will include bootstrap.js in the widget media in order to locate a jQuery and a jQuery-UI +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. First one wins: @@ -316,7 +318,7 @@ 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): +###### can_add(self, user, argmodel): Check if the user has permission to add one of these models. This enables the green popup + @@ -335,7 +337,7 @@ Also you could choose to return HttpResponseForbidden("who are you?") instead of admin.py -------- -#### make_ajax_form(model,fieldlist,superclass=ModelForm,show_help_text=False) +#### 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. @@ -354,16 +356,16 @@ If your application does not otherwise require a custom Form class then you can class YourModelAdmin(AjaxSelectAdmin): # create an ajax form class using the factory function - # model,fieldlist, [form superclass] - form = make_ajax_form(Label,{'owner':'person'}) + # 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) + 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. @@ -374,7 +376,7 @@ 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) +#### 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. @@ -402,7 +404,7 @@ A factory function to makes an ajax field + widget. The helper ensures things a class Meta: model = Release - group = make_ajax_field(Release,'group','group',help_text=None) + group = make_ajax_field(Release, 'group', 'group', help_text=None) #### Without using the helper @@ -419,7 +421,7 @@ A factory function to makes an ajax field + widget. The helper ensures things a class ReleaseForm(ModelForm): - group = AutoCompleteSelectField('group', required=False, help_text=None,plugin_options = {'autoFocus':True,'minLength':4}) + group = AutoCompleteSelectField('group', required=False, help_text=None, plugin_options = {'autoFocus': True, 'minLength': 4}) #### Using ajax selects in a `FormSet` @@ -442,7 +444,7 @@ There is possibly a better way to do this, but here is an initial example: 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) + TaskFormSet = modelformset_factory(Task, fields=('name', 'project', 'area'),extra=0, formset=BaseTaskFormSet) From 221e8614f0670db74463e278e84cdad788206e86 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sat, 5 Oct 2013 20:17:48 +0200 Subject: [PATCH 32/40] version bump --- ajax_select/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ajax_select/__init__.py b/ajax_select/__init__.py index 8e68c657cd..7bdf8b6550 100644 --- a/ajax_select/__init__.py +++ b/ajax_select/__init__.py @@ -1,5 +1,5 @@ """JQuery-Ajax Autocomplete fields for Django Forms""" -__version__ = "1.2.5" +__version__ = "1.3.0" __author__ = "crucialfelix" __contact__ = "crucialfelix@gmail.com" __homepage__ = "https://github.com/crucialfelix/django-ajax-selects/" From 4550bee6e878f41273a4c5afb90324d3bfc9a25d Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sun, 6 Oct 2013 18:22:32 +0200 Subject: [PATCH 33/40] jshint cleanup --- .jshintrc | 25 ++ .../static/ajax_select/js/ajax_select.js | 335 +++++++++--------- 2 files changed, 189 insertions(+), 171 deletions(-) create mode 100644 .jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000000..cc19e55e05 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,25 @@ +{ + "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 + } +} diff --git a/ajax_select/static/ajax_select/js/ajax_select.js b/ajax_select/static/ajax_select/js/ajax_select.js index 1eef9a4706..c91392805d 100644 --- a/ajax_select/static/ajax_select/js/ajax_select.js +++ b/ajax_select/static/ajax_select/js/ajax_select.js @@ -1,183 +1,176 @@ +'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'); + + 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'); + }); + } + + 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'); + } + 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'); + }); + } + + function kill(pk) { + $this.val($this.val().replace('|' + pk + '|', '|')); + $('#' + id+'_on_deck_'+pk).fadeOut().remove(); + } + + options.select = receiveResult; + $text.autocomplete(options); -if(typeof jQuery.fn.autocompleteselect !== 'function') { - -(function($) { - -$.fn.autocompleteselect = function(options) { - - return this.each(function() { - var id = this.id; - var $this = $(this); - - var $text = $("#"+id+"_text"); - var $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"); - - return false; - } - - function addKiller(repr,pk) { - 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"); - }); - } - - function kill() { - $this.val(''); - $deck.children().fadeOut(1.0).remove(); - } - - options.select = receiveResult; - $text.autocomplete(options); - - if (options.initial) { - its = options.initial; - addKiller(its[0], its[1]); - } - - $this.bind('didAddPopup', function(event, pk, repr) { - ui = { item: { pk: pk, repr: repr } } - receiveResult(null, ui); - }); - }); -}; - -$.fn.autocompleteselectmultiple = function(options) { - return this.each(function() { - var id = this.id; - - var $this = $(this); - var $text = $("#"+id+"_text"); - var $deck = $("#"+id+"_on_deck"); - - function receiveResult(event, ui) { - 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"); - } - - return false; - } - - function addKiller(repr, pk) { - killer_id = "kill_" + pk + id; - killButton = 'X '; - $deck.append('
' + killButton + repr + '
'); - - $("#"+killer_id).click(function() { - kill(pk); - $deck.trigger("killed"); - }); - } - - 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) { - ui = { item: { pk: pk, repr: repr } } - receiveResult(null, ui); - }); - }); -}; - -function addAutoComplete (inp, callback) { /*(html_id)*/ + 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); -} - - -/* 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 = windowname_to_id(win.name); - $("#"+name).trigger('didAddPopup',[html_unescape(newId),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"); - return false; - }; - $inp.autocomplete(opts); - }); + 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); + } + + + /* 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'); + 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=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); - }); + $('input[data-ajax-select=autocompleteselectmultiple]').each(function (i, inp) { + addAutoComplete(inp, function ($inp, opts) { + $inp.autocompleteselectmultiple(opts); + }); }); -}); - -$(document).ready(function() { - $(window).trigger("init-autocomplete"); -}); + }); -})(jQuery); + $(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); From 40651d72012c06b4c633b44091804c56a393bade Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Sun, 6 Oct 2013 22:08:54 +0200 Subject: [PATCH 34/40] fix setup.py to correctly include static/ when installing the egg (files would appear correctly in the sdist, but not when later installed from that source) --- MANIFEST.in | 1 - setup.py | 27 ++++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 36408f905d..f04bd56aaa 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -recursive-include ajax_select *.css *.py *.gif *.html *.txt *.js *.md recursive-include example *.py *.sh *.txt prune example/AJAXSELECTS prune example/ajax_select \ No newline at end of file diff --git a/setup.py b/setup.py index 45c1cdddcb..14a4eb9229 100644 --- a/setup.py +++ b/setup.py @@ -7,15 +7,28 @@ use_setuptools() from setuptools import setup -setup(name='django-ajax-selects', - version='1.2.5', +setup( + name='django-ajax-selects', + version='1.3.0', description='jQuery-powered auto-complete fields for editing ForeignKey, ManyToManyField and CharField', author='crucialfelix', author_email='crucialfelix@gmail.com', url='https://github.com/crucialfelix/django-ajax-selects/', - packages=['ajax_select', ], - package_data={'ajax_select': ['*.py','*.txt','static/css/*','static/images/*','static/js/*','templates/*.html', 'templates/ajax_select/*.html']}, - classifiers = [ + packages=['ajax_select'], + package_data={'ajax_select': + [ + '*.py', + '*.txt', + 'static/ajax_select/css/*', + 'static/ajax_select/images/*', + 'static/ajax_select/js/*', + 'templates/*.html', + 'templates/ajax_select/*.html' + ] + }, + include_package_data=True, + zip_safe=False, + classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2", "Development Status :: 5 - Production/Stable", @@ -27,7 +40,7 @@ "Topic :: Software Development :: User Interfaces", "Framework :: Django", ], - long_description = """\ + long_description="""\ Enables editing of `ForeignKey`, `ManyToManyField` and `CharField` using jQuery UI AutoComplete. 1. The user types a search term into the text field @@ -37,7 +50,7 @@ 5. Selected result displays in the "deck" area directly below the input field. 6. User can click trashcan icon to remove a selected item -+ Django 1.2+ ++ Django 1.4+ + Optional boostrap mode allows easy installation by automatic inclusion of jQueryUI from the googleapis CDN + Compatible with staticfiles, appmedia, django-compressor etc + Popup to add a new item is supported From b5f38b91e08d1fa16bd55f33b61f0837418b4aea Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 8 Oct 2013 12:34:33 +0200 Subject: [PATCH 35/40] example app: testing help text --- example/example/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/example/models.py b/example/example/models.py index 6c5981ed60..206098ee0e 100644 --- a/example/example/models.py +++ b/example/example/models.py @@ -17,7 +17,7 @@ class Group(models.Model): """ a music group """ - name = models.CharField(max_length=200, unique=True) + name = models.CharField(max_length=200, unique=True, help_text="Name of the group") members = models.ManyToManyField(Person, blank=True, help_text="Enter text to search for and add each member of the group.") url = models.URLField(blank=True) From 06111cf6abb7b286632a529d5a36243ab2799158 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 8 Oct 2013 12:34:46 +0200 Subject: [PATCH 36/40] pylint --- ajax_select/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ajax_select/admin.py b/ajax_select/admin.py index 715435dfcf..0dae1872f4 100644 --- a/ajax_select/admin.py +++ b/ajax_select/admin.py @@ -18,6 +18,6 @@ def get_form(self, request, obj=None, **kwargs): class AjaxSelectAdminTabularInline(admin.TabularInline): def get_formset(self, request, obj=None, **kwargs): - fs = super(AjaxSelectAdminTabularInline, self).get_formset(request, obj,**kwargs) + fs = super(AjaxSelectAdminTabularInline, self).get_formset(request, obj, **kwargs) autoselect_fields_check_can_add(fs.form, self.model, request.user) return fs From 67ae40c3c6a9811e05bccb3be52df8c9e7f3510c Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 8 Oct 2013 13:37:45 +0200 Subject: [PATCH 37/40] reinstate: allow html in the dropdown menu this was a feature using the old jquery autocomplete. --- ajax_select/fields.py | 4 +++ .../static/ajax_select/js/ajax_select.js | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/ajax_select/fields.py b/ajax_select/fields.py index a1ccc6c072..8f7c94e457 100644 --- a/ajax_select/fields.py +++ b/ajax_select/fields.py @@ -399,6 +399,10 @@ def plugin_options(channel, channel_name, widget_plugin_options, initial): 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)), diff --git a/ajax_select/static/ajax_select/js/ajax_select.js b/ajax_select/static/ajax_select/js/ajax_select.js index c91392805d..5dc0326193 100644 --- a/ajax_select/static/ajax_select/js/ajax_select.js +++ b/ajax_select/static/ajax_select/js/ajax_select.js @@ -129,6 +129,36 @@ 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 From 9c3bcfb6d98173f0c1e8bda6cf3d564126515022 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 8 Oct 2013 13:38:18 +0200 Subject: [PATCH 38/40] help message in example app --- example/example/lookups.py | 3 ++- example/example/models.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/example/lookups.py b/example/example/lookups.py index c9b62acbf3..31ddcc2742 100644 --- a/example/example/lookups.py +++ b/example/example/lookups.py @@ -18,7 +18,8 @@ def get_result(self, obj): def format_match(self, obj): """ (HTML) formatted item for display in the dropdown """ - return self.format_item_display(obj) + return u"%s
    %s
    " % (escape(obj.name), escape(obj.email)) + # return self.format_item_display(obj) def format_item_display(self, obj): """ (HTML) formatted item for displaying item in the selected deck area """ diff --git a/example/example/models.py b/example/example/models.py index 206098ee0e..c635752420 100644 --- a/example/example/models.py +++ b/example/example/models.py @@ -82,7 +82,7 @@ class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100) about_group = models.ForeignKey(Group) - mentions_persons = models.ManyToManyField(Person) + mentions_persons = models.ManyToManyField(Person, help_text="Person lookup renders html in menu") def __unicode__(self): return self.title From a6e2ba7233a2acdd4848456d22f8f40bba114145 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 8 Oct 2013 13:38:30 +0200 Subject: [PATCH 39/40] pylint --- ajax_select/fields.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ajax_select/fields.py b/ajax_select/fields.py index 8f7c94e457..65e28743e0 100644 --- a/ajax_select/fields.py +++ b/ajax_select/fields.py @@ -21,7 +21,7 @@ def _media(self): # where needed js = ('ajax_select/js/bootstrap.js', 'ajax_select/js/ajax_select.js') try: - if settings.AJAX_SELECT_BOOTSTRAP == False: + if not settings.AJAX_SELECT_BOOTSTRAP: js = ('ajax_select/js/ajax_select.js',) except AttributeError: pass @@ -373,8 +373,7 @@ def _check_can_add(self, user, model): 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()}) + 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): From 10c65a07021bd3e08e6ea236f70e442808e508b4 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 8 Oct 2013 13:38:42 +0200 Subject: [PATCH 40/40] better formatting in install.sh --- example/install.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/example/install.sh b/example/install.sh index dd0d439f8e..7fce4d2d9f 100755 --- a/example/install.sh +++ b/example/install.sh @@ -9,28 +9,28 @@ source AJAXSELECTS/bin/activate DJANGO=$1 if [ "$DJANGO" != "" ]; then - echo "Installing Django $DJANGO" + echo "Installing Django $DJANGO:" pip install Django==$DJANGO else - echo "Installing latest django" + echo "Installing latest django:" pip install django fi -echo "Creating a sqllite database" +echo "Creating a sqllite database:" ./manage.py syncdb if [ ! -d ./ajax_select ]; then - echo "\nSymlinking ajax_select into this app directory" + echo "\nSymlinking ajax_select into this app directory:" ln -s ../ajax_select/ ./ajax_select fi -echo "\ntype 'source AJAXSELECTS/bin/activate' to activate the virtualenv" +echo "\nto activate the virtualenv:\nsource AJAXSELECTS/bin/activate" -echo '\ncreate an admin account:' +echo '\nto create an admin account:' echo './manage.py createsuperuser' -echo "\nrun: ./manage.py runserver" -echo "and visit http://127.0.0.1:8000/admin/" -echo "\ntype 'deactivate' to close the virtualenv or just close the shell" +echo "\nto run the testserver:\n./manage.py runserver" +echo "\nthen open this url:\nhttp://127.0.0.1:8000/admin/" +echo "\nto close the virtualenv or just close the shell:\ndeactivate" exit 0