Last active
December 22, 2015 04:38
-
-
Save asermax/6418178 to your computer and use it in GitHub Desktop.
Related selects widget for Django
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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