Skip to content

Instantly share code, notes, and snippets.

@asermax
Last active December 22, 2015 04:38
Show Gist options
  • Save asermax/6418178 to your computer and use it in GitHub Desktop.
Save asermax/6418178 to your computer and use it in GitHub Desktop.
Related selects widget for Django
from django import forms
from your_app import widgets, models
class YourModelModelForm(forms.ModelForm):
class Meta:
model = models.YourModel
widgets = {
'your_field': widgets.RelatedModelSelect(
[models.GeneralModel, models.SpecificModel] # order is important here, the model that defines
), # your_field should be the **last** in the list
}
from django.http import HttpResponseBadRequest, HttpResponse
from django.utils import simplejson
from django.db.models.loading import get_model
JSON_MODELS = {
'Model': 'filter_field',
}
def all_json_models(request, model, pk):
''' Devuelve todos los objectos de un modelo en particular, filtrados
por un valor predeterminado, en formato JSON.
'''
if model not in JSON_MODELS:
return HttpResponseBadRequest()
model_class = get_model('application', model)
related = JSON_MODELS[model]
json = simplejson.dumps(
[{'value': o.pk, 'display': str(o)}
for o in model_class.objects.filter(**{related + '__pk': pk})])
return HttpResponse(json, mimetype="application/javascript")
from django import forms
from django.db import models
def get_related_field(model, related_model):
for field in model._meta.fields:
if isinstance(field, models.ForeignKey) and \
field.rel.to is related_model:
return field
class RelatedSelect(forms.Select):
class Media:
js = ('js/related-model.js',)
def __init__(self, related_select=None, model=None, attrs=None,
choices=()):
forms.Select.__init__(self, attrs=attrs, choices=choices)
if related_select:
self.related_select = related_select
if model:
self.model = model
@property
def related_select(self):
if 'related_select' in self.attrs:
return self.attrs['related_select']
@related_select.setter
def related_select(self, related):
self.attrs['related_select'] = related
@property
def model(self):
if 'model' in self.attrs:
return self.attrs['model']
@model.setter
def model(self, model):
self.attrs['model'] = model
class ChoiceIterator(object):
'''
Iterador/Generador que a partir de una lista de instancias de un modelo
genera choices para un widget Select.
'''
def __init__(self, models=None, empty_label='----------'):
models = models or []
# de cada instancia saca el id y el objeto en si
self.choices = [(obj.id, obj) for obj in models]
# el primer elemento siempre es un placeholder
self.choices.insert(0, (u'', empty_label))
def __iter__(self):
for choice in self.choices:
yield choice
def __len__(self):
return len(self._models)
class RelatedModelSelect(forms.MultiWidget):
'''
Este widget permite realizar una selección de un elemento que depende de
selecciones previas en modelos relacionados.
:param models: `list` de las `class` de los diferentes modelos que forman
parte de la seleccion, en orden jerárquico (por ej, en el caso de la
marca y el modelo de un auto, seria [Marca, Modelo].
:param separator: `str` con el separador para los widgets. Por defecto se
colocan uno junto al otro.
:param label_format: `str` con el formato para el label. Por defecto es
"nombre_modelo: ".
'''
def __init__(self, models, separator=u'', auto_labels=True, labels=None,
attrs=None):
if len(models) <= 1:
raise ValueError('La clase RelatedModelSelect solo se debe ' +
'utilizar con dos o mas modelos')
self.models = models
self.separator = separator
self.auto_labels = auto_labels
self.labels = labels
# generamos los Select widgets con un choice iterator vacio
_widgets = \
[RelatedSelect(choices=ChoiceIterator(
empty_label=model._meta.verbose_name.capitalize()),
attrs=attrs,
model=model.__name__) for model in models]
# inicializamos las choices
self._init_first_choices(_widgets)
super(RelatedModelSelect, self).__init__(_widgets, attrs)
def __deepcopy__(self, memo):
result = super(forms.MultiWidget, self).__deepcopy__(memo)
# hay que recargar las choices por que si no se cachean
result._init_first_choices()
return result
def _init_first_choices(self, widgets=None):
# el widget inicial siempre carga todos los elementos ya que el primer
# modelo es el que lidera la jerarquía
widgets = widgets or self.widgets
widgets[0].choices = ChoiceIterator(self.models[0].objects.all(),
self.models[0]._meta.verbose_name.capitalize())
def decompress(self, value):
if value:
# obtenemos el objeto seleccionado
value = self.models[-1].objects.get(id=value)
# el primer valor siempre va a ser el valor guardado
value_list = [value.id]
for i in range(len(self.models) - 1, 0, -1):
# recorremos los modelos de atras para adelante, para ir
# recuperando los valores correspondientes al modelo previo
curr_mod = self.models[i]
prev_mod = self.models[i - 1]
# recuperamos el campo que relaciona los dos modelos
# FIXME: si existen dos relaciones esto no funciona
related_field = get_related_field(curr_mod, prev_mod)
if related_field:
# obtenemos el valor que corresponde...
value = getattr(value, related_field.name)
value_list.insert(0, value.id)
# ...y cargamos las choices para el widget correspondiente
manager_name = related_field.related.get_accessor_name()
widget = self.widgets[i]
widget.choices = ChoiceIterator(
getattr(value, manager_name).all())
else:
raise Exception()
return value_list
return [None] * len(self.models)
def format_output(self, rendered_widgets):
fmt = '<label for=id_%s_%d">%s</label>\n'
if self.auto_labels and not self.labels:
# si esta seteado auto_labels, generamos labels automaticas
label_format = '%s: '
self.labels = [label_format % model._meta.verbose_name.title()
for model in self.models]
if self.labels:
for i in range(0, len(rendered_widgets)):
widget = rendered_widgets[i]
rendered_widgets[i] = fmt % (self._name, i, self.labels[i])\
+ widget
# generamos el separador
separator = u'\n%s\n' % self.separator
# generamos el html final con el separador generado
return separator.join(rendered_widgets)
def render(self, name, value, attrs=None):
# guardamos el nombre que se usa para los labels
self._name = name
# agregamos los attrs que hacen falta para que funcione la recuperacion
# asincrona de valores relacionados
for i in range(1, len(self.widgets)):
self.widgets[i].related_select = "%s_%i" % (name, i - 1)
return super(RelatedModelSelect, self).render(name, value, attrs)
def value_from_datadict(self, data, files, name):
last_value = super(RelatedModelSelect, self).value_from_datadict(data,
files, name)[-1]
return last_value if last_value != '' else None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment