Skip to content

Default queryset support #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 69 additions & 59 deletions flexselect/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from collections import defaultdict
from itertools import chain
import hashlib
import json
Expand All @@ -15,44 +14,53 @@
FLEXSELECT = {
'include_jquery': False,
}
try: FLEXSELECT.update(settings.FLEXSELECT)
except AttributeError: pass


try:
FLEXSELECT.update(settings.FLEXSELECT)
except AttributeError:
pass


def choices_from_queryset(queryset):
"""
Makes choices from a QuerySet in a format that is usable by the
Makes choices from a QuerySet in a format that is usable by the
django.forms.widget.Select widget.

queryset: An instance of django.db.models.query.QuerySet
"""
return chain(
[EMPTY_CHOICE],
[(o.pk, smart_unicode(o)) for o in queryset],
)


def choices_from_instance(instance, widget):
"""
Builds choices from a model instance using the widgets queryset() method.
If any of the widgets trigger_field fields is not defined on the instance or
Builds choices from a model instance using the widgets queryset() method.
If any of the widgets trigger_field fields is not defined on the instance or
the instance itself is None, None is returned.

instance: An instance of the model used on the current admin page.
widget: A widget instance given to the FlexSelectWidget.
"""
try:
for trigger_field in widget.trigger_fields:
getattr(instance, trigger_field)
except (ObjectDoesNotExist, AttributeError):
return [('', widget.empty_choices_text(instance))]

try:
return choices_from_queryset(widget.default_queryset(instance))
except NotImplementedError:
return [('', widget.empty_choices_text(instance))]

return choices_from_queryset(widget.queryset(instance))



def details_from_instance(instance, widget):
"""
Builds html from a model instance using the widgets details() method. If
any of the widgets trigger_field fields is not defined on the instance or
any of the widgets trigger_field fields is not defined on the instance or
the instance itself is None, None is returned.

instance: An instance of the model used on the current admin page.
widget: A widget instance given to the FlexSelectWidget.
"""
Expand All @@ -64,58 +72,60 @@ def details_from_instance(instance, widget):
return u''
return widget.details(related_instance, instance)


def instance_from_request(request, widget):
"""
Returns a partial instance of the widgets model loading it with values
from a POST request.
"""
items = dict(request.POST.items())
values = {}
for f in widget.base_field.model._meta.fields:
if f.name in items:
try:
value = f.formfield().to_python(items[f.name])
if value is not None:
values[f.name] = value
except ValidationError:
pass
return widget.base_field.model(**values)

"""
Returns a partial instance of the widgets model loading it with values
from a POST request.
"""
items = dict(request.POST.items())
values = {}
for f in widget.base_field.model._meta.fields:
if f.name in items:
try:
value = f.formfield().to_python(items[f.name])
if value is not None:
values[f.name] = value
except ValidationError:
pass
return widget.base_field.model(**values)


class FlexSelectWidget(Select):
"""Instances of widgets with their hashed names as keys."""
instances = {}
""" Instances of widgets with their hashed names as keys."""


class Media:
js = []
if FLEXSELECT['include_jquery']:
googlecdn = "https://ajax.googleapis.com/ajax/libs"
js.append('%s/jquery/1.6.1/jquery.min.js' % googlecdn)
js.append('%s/jqueryui/1.8.13/jquery-ui.min.js' % googlecdn)
js.append('flexselect/js/flexselect.js')
def __init__(self, base_field, modeladmin, request, *args,

def __init__(self, base_field, modeladmin, request, *args,
**kwargs):

self.base_field = base_field
self.modeladmin = modeladmin
self.request = request

self.hashed_name = self._hashed_name()
FlexSelectWidget.instances[self.hashed_name] = self
super(FlexSelectWidget, self).__init__(*args, **kwargs)

def _hashed_name(self):
"""
Each widget will be unique by the name of the field and the class name
Each widget will be unique by the name of the field and the class name
of the model admin.
"""
salted_string = "".join([
settings.SECRET_KEY,
self.base_field.name,
self.modeladmin.__class__.__name__,
settings.SECRET_KEY,
self.base_field.name,
self.modeladmin.__class__.__name__,
])
return "_%s" % hashlib.sha1(salted_string).hexdigest()

def _get_instance(self):
"""
Returns a model instance from the url in the admin page.
Expand All @@ -129,7 +139,7 @@ def _get_instance(self):
return self.modeladmin.get_object(self.request, object_id)
except ValueError:
return None

def _build_js(self):
"""
Adds the widgets hashed_name as the key with an array of its trigger_fields
Expand All @@ -141,42 +151,42 @@ def _build_js(self):
flexselect.fields = flexselect.fields || {};
flexselect.fields.%s = %s;
</script>""" % (
self.hashed_name, json.dumps({
'base_field': self.base_field.name,
'trigger_fields': self.trigger_fields,})
)


self.hashed_name, json.dumps({
'base_field': self.base_field.name,
'trigger_fields': self.trigger_fields
}))

def render(self, name, value, attrs=None, choices=(), *args, **kwargs):
"""
Overrides. Reduces the choices by calling the widgets queryset()
method and adds a details <span> that is filled with the widgets
Overrides. Reduces the choices by calling the widgets queryset()
method and adds a details <span> that is filled with the widgets
details() method.
"""
instance = self._get_instance()
self.choices = choices_from_instance(instance, self)
html = []
html.append(super(FlexSelectWidget, self).render(
name, value, attrs=attrs,
name, value, attrs=attrs,
*args, **kwargs
))
html.append(self._build_js())
html.append('<span class="flexselect_details">')
html.append(details_from_instance(instance, self))
html.append('</span>')
return mark_safe("".join(html))

# Methods and properties that must be implemented.

trigger_fields = []

def details(self, base_field_instance, instance):
raise NotImplementedError

def queryset(self, instance):
raise NotImplementedError


def default_queryset(self, instance):
raise NotImplementedError

def empty_choices_text(self, instance):
raise NotImplementedError


4 changes: 2 additions & 2 deletions flexselect/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.conf.urls.defaults import *

urlpatterns = patterns('flexselect.views',
(r'field_changed', 'field_changed'),
urlpatterns = patterns(
'flexselect.views', (r'field_changed', 'field_changed'),
)
13 changes: 7 additions & 6 deletions flexselect/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,27 @@
from django.forms.widgets import Select
from django.contrib.auth.decorators import login_required

from flexselect import (FlexSelectWidget, choices_from_instance,
from flexselect import (FlexSelectWidget, choices_from_instance,
details_from_instance, instance_from_request)


@login_required
def field_changed(request):
"""
Ajax callback called when a trigger field or base field has changed. Returns
html for new options and details for the dependent field as json.
"""
hashed_name = request.POST.__getitem__('hashed_name')
widget = FlexSelectWidget.instances[hashed_name]
widget = FlexSelectWidget.instances[hashed_name]
instance = instance_from_request(request, widget)

if bool(int(request.POST.__getitem__('include_options'))):
choices = choices_from_instance(instance, widget)
options = Select(choices=choices).render_options([], [])
else:
options = None

return HttpResponse(json.dumps({
'options' : options,
'options': options,
'details': details_from_instance(instance, widget),
}), mimetype='application/json')
}), mimetype='application/json')