Database field | Form field | Form widget | -
---|---|---|
models.CharField | AutoCompleteField | AutoCompleteWidget |
models.ForeignKey | AutoCompleteSelectField | AutoCompleteSelectWidget |
models.ManyToManyField | AutoCompleteSelectMultipleField | AutoCompleteSelectMultipleWidget |
form Field | tries this first | default template |
---|---|---|
AutoCompleteField | templates/autocomplete_{{CHANNELNAME}}.html | templates/autocomplete.html |
AutoCompleteSelectField | templates/autocompleteselect_{{CHANNELNAME}}.html | templates/autocompleteselect.html |
AutoCompleteSelectMultipleField | templates/autocompleteselectmultiple_{{CHANNELNAME}}.html | templates/autocompleteselectmultiple.html |
+ You could put additional UI or help text here. +
+ {% endblock %} diff --git a/docs/source/Example-app.rst b/docs/source/Example-app.rst new file mode 100644 index 0000000000..f734d4f918 --- /dev/null +++ b/docs/source/Example-app.rst @@ -0,0 +1,11 @@ + +Example App +=========== + +Clone this repository and 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. + +install:: + + cd example + ./install.sh 1.8.5 + ./manage.py runserver diff --git a/docs/source/Forms.rst b/docs/source/Forms.rst new file mode 100644 index 0000000000..d474056a31 --- /dev/null +++ b/docs/source/Forms.rst @@ -0,0 +1,65 @@ +Forms +===== + +Forms can be used either for an Admin or in normal Django views. + +Subclass ModelForm as usual and define fields:: + + from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField + + class DocumentForm(ModelForm): + + class Meta: + model = Document + + category = AutoCompleteSelectField('categories', required=False, help_text=None) + tags = AutoCompleteSelectMultipleField('tags', required=False, help_text=None) + + +make_ajax_field +--------------- + +There is also a helper method available here. + +.. automodule:: ajax_select.helpers + :members: make_ajax_field + :noindex: + + +Example:: + + from ajax_select import make_ajax_field + + class DocumentForm(ModelForm): + + class Meta: + model = Document + + category = make_ajax_field(Category, 'categories', 'category', help_text=None) + tags = make_ajax_field(Tag, 'tags', 'tags', help_text=None) + + + +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 Task + + # 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) diff --git a/docs/source/Install.rst b/docs/source/Install.rst new file mode 100644 index 0000000000..d18d9dd627 --- /dev/null +++ b/docs/source/Install.rst @@ -0,0 +1,90 @@ +Install +======= + +Install:: + + pip install django-ajax-selects + +Add the app:: + + # settings.py + INSTALLED_APPS = ( + ... + 'ajax_select', # <- add the app + ... + ) + +Include the urls in your project:: + + # urls.py + from django.conf.urls import url, include + 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 = [ + + # place it at whatever base url you like + url(r'^ajax_select/', include(ajax_select_urls)), + + url(r'^admin/', include(admin.site.urls)), + ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + +Write a LookupChannel to specify the models, search queries, formatting etc. and register it with a channel name:: + + from ajax_select import register, LookupChannel + from .models import Tag + + @register('tags') + class TagsLookup(LookupChannel): + + model = Tag + + def get_query(self, q, request): + return self.model.objects.filter(name=q) + + def format_item_display(self, item): + return u"%s" % item.name + +If you are using Django >= 1.7 then it will automatically loaded on startup. +For previous Djangos you can import them manually to your urls or views. + +Add ajax lookup fields in your admin.py:: + + from django.contrib import admin + from ajax_select import make_ajax_form + from .models import Document + + @admin.register(Document) + class DocumentAdmin(AjaxSelectAdmin): + + form = make_ajax_form(Document, { + # fieldname: channel_name + 'tags': 'tags' + }) + +Or add the fields to a ModelForm:: + + # forms.py + from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField + + class DocumentForm(ModelForm): + + class Meta: + model = Document + + category = AutoCompleteSelectField('categories', required=False, help_text=None) + tags = AutoCompleteSelectMultipleField('tags', required=False, help_text=None) + + # admin.py + from django.contrib import admin + from .forms import DocumentForm + from .models import Document + + @admin.register(Document) + class DocumentAdmin(AjaxSelectAdmin): + form = DocumentForm diff --git a/docs/source/LookupChannel.rst b/docs/source/LookupChannel.rst new file mode 100644 index 0000000000..97c3f12053 --- /dev/null +++ b/docs/source/LookupChannel.rst @@ -0,0 +1,77 @@ +Lookup Channels +=============== + +A LookupChannel defines how to search and how to format found objects for display in the interface. + +LookupChannels are registered with a "channel name" and fields refer to that name. + +You may have only one LookupChannel for a model, or you might define several for the same Model each with different queries, security policies and formatting styles. + +Custom templates can be created for channels. This enables adding extra javascript or custom UI. See :doc:`/Custom-Templates` + + +lookups.py +---------- + +Write your LookupChannel classes in a file name `yourapp/lookups.py` + +(note: inside your app, not your top level project) + +Use the @register decorator to register your LookupChannels by name + +`example/lookups.py`:: + + from ajax_select import register, LookupChannel + + @register('things') + class ThingsLookup(LookupChannel): + + model = Things + + def get_query(self, q, request): + return self.model.objects.filter(title__icontains=q).order_by('title') + + +If you are using Django >= 1.7 then all `lookups.py` in all of your apps will be automatically imported on startup. + +If Django < 1.7 then you can import each of your lookups in your views or urls. +Or you can register them in settings (see below). + +Customize +--------- + +.. automodule:: ajax_select.lookup_channel + :members: LookupChannel + :noindex: + + +settings.py +----------- + +Versions previous to 1.4 loaded the LookupChannels according to `settings.AJAX_LOOKUP_CHANNELS` + +This will still work. Your LookupChannels will continue to load without having to add them with the new @register decorator. + +Example:: + + # settings.py + + AJAX_LOOKUP_CHANNELS = { + # auto-create a channel named 'person' that searches by name on the model Person + # str: dict + 'person': {'model': 'example.person', 'search_field': 'name'} + + # specify a lookup to be loaded + # str: tuple + 'song': ('example.lookups', 'SongLookup'), + + # delete a lookup channel registered by an app/lookups.py + # str: None + 'users': None + } + + +One situation where it is still useful: if a resuable app defines a LookupChannel and you want to override that or turn it off. +Pass None as in the third example above. + +Anything in `settings.AJAX_LOOKUP_CHANNELS` overwrites anything previously registered by an app. diff --git a/docs/source/Media-assets.rst b/docs/source/Media-assets.rst new file mode 100644 index 0000000000..fdb2b64266 --- /dev/null +++ b/docs/source/Media-assets.rst @@ -0,0 +1,39 @@ +Media Assets +============ + +If `jQuery` or `jQuery.ui` are not already loaded on the page, then these will be loaded from CDN:: + + ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js + code.jquery.com/ui/1.10.3/jquery-ui.js + code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css + +If you want to prevent this and load your own then set:: + + # settings.py + AJAX_SELECT_BOOTSTRAP = False + + +Customizing the style sheet +--------------------------- + +By default `css/ajax_select.css` is included by the Widget's media. This specifies a simple basic style. + +If you would prefer not to have `css/ajax_select.css` loaded at all then you can implement your own `yourapp/static/ajax_select/css/ajax_select.css` and put your app before `ajax_select` in `INSTALLED_APPS`. + +Your version will take precedence and Django will serve your `css/ajax_select.css` + +The markup is simple and you can just add more css to override unwanted styles. + +The trashcan icon comes from the jQueryUI theme by the css classes:: + + "ui-icon ui-icon-trash" + +The following 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. + +The loading indicator is in `ajax_select/static/ajax_select/images/loading-indicator.gif` + +`yourapp/static/ajax_select/images/loading-indicator.gif` would override that. diff --git a/docs/source/Ordered-ManyToMany.rst b/docs/source/Ordered-ManyToMany.rst new file mode 100644 index 0000000000..e43677b4fa --- /dev/null +++ b/docs/source/Ordered-ManyToMany.rst @@ -0,0 +1,61 @@ + +Ordered ManyToMany fields +========================= + +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. + + +Problem +------- + +Given these models:: + + 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. + +But when Django ORM retrieves them, the order is not guaranteed:: + + # 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 the join table pk + # and thus gets them in the order they were added. + apartment.agents.through.objects.filter(apt=self).select_related('agent').order_by('id') + + +Solution +-------- + +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. + +However this method will also work. + +Make a custom ManyToManyField:: + + from django.db import models + + class AgentOrderedManyToManyField(models.ManyToManyField): + + """This fetches from the join table, then fetches the Agents in the fixed id order.""" + + def value_from_object(self, object): + 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)) + return [agents[aid] for aid in aids if aid in agents] + + class Agent(models.Model): + name = models.CharField(blank=True, max_length=100) + + class Apartment(models.Model): + agents = AgentOrderedManyToManyField() diff --git a/docs/source/Outside-of-Admin.rst b/docs/source/Outside-of-Admin.rst new file mode 100644 index 0000000000..04b78c3a97 --- /dev/null +++ b/docs/source/Outside-of-Admin.rst @@ -0,0 +1,12 @@ +Outside of the Admin +==================== + +ajax_selects does not need to be in a Django 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. + +In your view, after creating your ModelForm object:: + + autoselect_fields_check_can_add(form, model, request.user) + +This will check each widget and enable the green + for them iff the User has permission. diff --git a/docs/source/Upgrading.rst b/docs/source/Upgrading.rst new file mode 100644 index 0000000000..59ee6ed810 --- /dev/null +++ b/docs/source/Upgrading.rst @@ -0,0 +1,41 @@ +Upgrading from previous versions +================================ + +1.4 + +Custom Templates +---------------- + +Move your custom templates from:: + + yourapp/templates/channel_autocomplete.html + yourapp/templates/channel_autocompleteselect.html + yourapp/templates/channel_autocompleteselectmultiple.html + +to:: + + yourapp/templates/ajax_select/channel_autocomplete.html + yourapp/templates/ajax_select/channel_autocompleteselect.html + yourapp/templates/ajax_select/channel_autocompleteselectmultiple.html + +And change your extends from:: + + {% extends "autocompleteselect.html" %} + +to:: + + {% extends "ajax_select/autocompleteselect.html" %} + + +Removed options +--------------- + +make_ajax_field: + show_m2m_help -> show_help_text + +settings +-------- + +LookupChannels are still loaded from `settings.AJAX_LOOKUP_CHANNELS` as previously. + +If you are on Django >= 1.7 you may switch to using the @register decorator and you can remove that setting. diff --git a/docs/source/_static/add-song.png b/docs/source/_static/add-song.png new file mode 100644 index 0000000000..f533dfe969 Binary files /dev/null and b/docs/source/_static/add-song.png differ diff --git a/docs/source/_static/add.png b/docs/source/_static/add.png new file mode 100644 index 0000000000..d80c259c76 Binary files /dev/null and b/docs/source/_static/add.png differ diff --git a/docs/source/_static/kiss-all.png b/docs/source/_static/kiss-all.png new file mode 100644 index 0000000000..b9522c8cc4 Binary files /dev/null and b/docs/source/_static/kiss-all.png differ diff --git a/docs/source/_static/kiss.png b/docs/source/_static/kiss.png new file mode 100644 index 0000000000..cddc02f2f8 Binary files /dev/null and b/docs/source/_static/kiss.png differ diff --git a/docs/source/ajax_select.rst b/docs/source/ajax_select.rst new file mode 100644 index 0000000000..5ee8a103ad --- /dev/null +++ b/docs/source/ajax_select.rst @@ -0,0 +1,86 @@ +ajax_select package +=================== + +Submodules +---------- + +ajax_select.admin module +------------------------ + +.. automodule:: ajax_select.admin + :members: + :undoc-members: + :show-inheritance: + +ajax_select.apps module +----------------------- + +.. automodule:: ajax_select.apps + :members: + :undoc-members: + :show-inheritance: + +ajax_select.fields module +------------------------- + +.. automodule:: ajax_select.fields + :members: + :undoc-members: + :show-inheritance: + +ajax_select.helpers module +-------------------------- + +.. automodule:: ajax_select.helpers + :members: + :undoc-members: + :show-inheritance: + +ajax_select.lookup_channel module +--------------------------------- + +.. automodule:: ajax_select.lookup_channel + :members: + :undoc-members: + :show-inheritance: + +ajax_select.models module +------------------------- + +.. automodule:: ajax_select.models + :members: + :undoc-members: + :show-inheritance: + +ajax_select.registry module +--------------------------- + +.. automodule:: ajax_select.registry + :members: + :undoc-members: + :show-inheritance: + +ajax_select.urls module +----------------------- + +.. automodule:: ajax_select.urls + :members: + :undoc-members: + :show-inheritance: + +ajax_select.views module +------------------------ + +.. automodule:: ajax_select.views + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: ajax_select + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000000..75b55a1934 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +# +# django-ajax-selects documentation build configuration file, created by +# sphinx-quickstart on Tue Nov 3 15:23:14 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('../../')) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.example.settings') + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.napoleon' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'django-ajax-selects' +copyright = u'2015, Chris Sattinger' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.4.0' +# The full version, including alpha/beta/rc tags. +release = '1.4.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. + +try: + import sphinx_rtd_theme +except ImportError: + html_theme = 'default' +else: + html_theme = "sphinx_rtd_theme" + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "