diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..1412a46 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,16 @@ +Django CMS +========== +* Patrick Lauber +* Jason Zylks +* Simon Meers +* Peter Cicman + +Django Page CMS +=============== +* Batiste Bieler +* Jannis Leidel +* Antoni Aloy López +* Benjamin Wohlwend +* poweredbypenguins +* homebrew79 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2445846 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2009, Patrick Lauber +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the author nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a68e211 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +Django Smart Selects +==================== + +If you the following model: + + class Location(models.Model) + continent = models.ForeignKey(Continent) + country = models.ForeignKey(Country) + area = models.ForeignKey(Area) + city = models.CharField(max_length=50) + street = models.CharField(max_length=100) + +And you want that if you select a continent only the countries are available that are located on this continent and the same for areas +you can do the following: + +class Location(models.Model) + continent = models.ForeignKey(Continent) + country = ChainedForeignKey(Country, chained_field="continent", chained_model_field="continent") + area = ChainedForeignKey(Area, chained_field="country", chained_model_field="country") + city = models.CharField(max_length=50) + street = models.CharField(max_length=100) + +This example asumes that the Country Model has a "continent" field and that the Area model has "country" field. + +The chained field is the field on the same model the field should be chained too. +The chained model field is the field of the chained model that corresponds to the model linked too by the chained field. \ No newline at end of file diff --git a/smart_selects/__init__.py b/smart_selects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smart_selects/fields.py b/smart_selects/fields.py new file mode 100644 index 0000000..89de20f --- /dev/null +++ b/smart_selects/fields.py @@ -0,0 +1,118 @@ +from django.db.models.fields.related import ForeignKey +from django import forms +from django.forms.models import ModelChoiceField +from django.forms.fields import ChoiceField +from django.forms.widgets import Select +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.conf import settings +from django.db.models.loading import get_model + +JQ_URL = getattr(settings, 'JQUERY_URL', settings.MEDIA_URL + 'js/jquery/jquery-latest.js') + +class ChainedForeignKey(ForeignKey): + """ + chains the choices of a previous combo box with this one + """ + def __init__(self, to, chained_field, chained_model_field, *args, **kwargs): + self.app_name = to._meta.app_label + self.model_name = to._meta.object_name + self.chain_field = chained_field + self.model_field = chained_model_field + ForeignKey.__init__(self, to, *args, **kwargs) + + + def formfield(self, **kwargs): + defaults = { + 'form_class': ChainedModelChoiceField, + 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to), + 'to_field_name': self.rel.field_name, + 'app_name':self.app_name, + 'model_name':self.model_name, + 'chain_field':self.chain_field, + 'model_field':self.model_field, + } + defaults.update(kwargs) + return super(ChainedForeignKey, self).formfield(**defaults) + +class ChainedSelect(Select): + def __init__(self, app_name, model_name, chain_field, model_field, *args, **kwargs): + self.app_name = app_name + self.model_name = model_name + self.chain_field = chain_field + self.model_field = model_field + super(Select, self).__init__(*args, **kwargs) + + class Media: + js = ( + JQ_URL, + ) + + def render(self, name, value, attrs=None, choices=()): + url = "/".join(reverse("chained_filter", kwargs={'app':self.app_name,'model':self.model_name,'field':self.model_field,'value':"1"}).split("/")[:-2]) + js = """ + + + """ % {"chainfield":self.chain_field, "url":url, "id":attrs['id']} + final_choices=[] + if value: + item = self.queryset.filter(pk=value)[0] + pk = getattr(item, self.model_field+"_id") + filter={self.model_field:pk} + filtered = get_model( self.app_name, self.model_name).objects.filter(**filter) + for choice in filtered: + final_choices.append((choice.pk, unicode(choice))) + for choice in self.choices: + self.choices = [choice] + break + output = super(ChainedSelect, self).render(name, value, attrs, choices=final_choices) + output += js + return mark_safe(output) + + +class ChainedModelChoiceField(ModelChoiceField): + def __init__(self, app_name, model_name, chain_field, model_field, initial=None, *args, **kwargs): + defaults = {'widget':ChainedSelect(app_name,model_name,chain_field,model_field)} + defaults.update(kwargs) + super(ChainedModelChoiceField, self).__init__(initial=initial, *args, **defaults) + + #widget = ChainedSelect + def _get_choices(self): + self.widget.queryset = self.queryset + choices = super(ChainedModelChoiceField, self)._get_choices() + return choices + if hasattr(self, '_choices'): + return self._choices + + final = [("","---------"),] + return final + choices = property(_get_choices, ChoiceField._set_choices) + + + diff --git a/smart_selects/models.py b/smart_selects/models.py new file mode 100644 index 0000000..e69de29 diff --git a/smart_selects/urls.py b/smart_selects/urls.py new file mode 100644 index 0000000..096d012 --- /dev/null +++ b/smart_selects/urls.py @@ -0,0 +1,5 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('chained_selects.views', + url(r'^(?P[\w\-]+)/(?P[\w\-]+)/(?P[\w\-]+)/(?P[\w\-]+)/$', 'filterchain', name='chained_filter'), +) \ No newline at end of file diff --git a/smart_selects/views.py b/smart_selects/views.py new file mode 100644 index 0000000..e30ca81 --- /dev/null +++ b/smart_selects/views.py @@ -0,0 +1,13 @@ +from django.db.models import get_model +from django.http import HttpResponse +from django.utils import simplejson + +def filterchain(request, app, model, field, value): + Model = get_model(app, model) + keywords = {str(field): str(value)} + results = Model.objects.filter(**keywords) + result = [] + for item in results: + result.append({'value':item.pk, 'display':unicode(item)}) + json = simplejson.dumps(result) + return HttpResponse(json, mimetype='application/json')