Filtrando y ordenando widget foreignkey Django Admin

Hace un tiempo en un poryecto nos pidieron ordenar por el nombre de la foreignkey, de una foreign key al mostrarla en el widget select del form de Django. La solución fué realizar para ello una función en el admin.py. Voy a poner un caso de ejemplo para entender como filtrar y ordenar el contenido de un select list dentro del django admin.

Imaginad que tenemos un proyecto en el que tenemos usuarios, que tienen asignado un cargo. Por defecto, en el Django Admin esto generará un select list en el formulario del usuario con todos los cargos de la BBDD. Esto es poco útil cuando son muy extensos, por ejemplo. En este post voy a explicar dos cosas:

  • Cómo modificar el orden de un select list para la foreign key.
  • Como mostrarlo directamente filtrado.

Tenemos dos modelos.
Por un lado, el usuario:

class Consumer(models.Model):
    activoChoices = (
        ('S', 'Si'),
        ('N', 'No')
    )
    codigo = models.IntegerField(
        primary_key=True, null=False, blank=False, editable=False)
    nombre = models.CharField(max_length=500, null=False, blank=True)
    apellido = models.CharField(max_length=500, null=False, blank=True)
    direccion = models.CharField(max_length=500, null=True, blank=True)
    poblacion = models.CharField(max_length=500, null=True, blank=True)
    provincia = models.IntegerField(null=True, blank=True, editable=True)
    nif = models.CharField(max_length=500, null=True, blank=True)
    activo = models.CharField(
        max_length=300,
        null=False,
        blank=False,
        choices=activoChoices,
        default='S'
    )  
    cargo = models.ForeignKey(
        Cargo, related_name='consumer_cargo', null=True, blank=True)

    def __unicode__(self):
        return self.nombre

    def save(self, *args, **kwargs):
        if not self.codigo:            
            no = Consumer.objects.count()
            if no == 0:
                self.codigo = 1
            else:
                self.codigo = self.__class__.objects.all().order_by(
                    "-codigo")[0].codigo + 1

        super(Consumer, self).save(*args, **kwargs)

Si os fijáis, tiene asociado un Cargo, como Foreign Key. Por otro lado, tenemos el propio Cargo:

class Cargo(models.Model):
    activoChoices = (
        ('S', 'Si'),
        ('N', 'No')
    )
    ID = models.IntegerField(
        primary_key=True, null=False, blank=False, editable=False)
    nombre = models.CharField(max_length=300, null=False, blank=False)
    activo = models.CharField(
        max_length=300,
        null=False,
        blank=False,
        choices=activoChoices,
        default='S'
    )      
    def __unicode__(self):
        return self.nombre

    def save(self, *args, **kwargs):
        if not self.ID:
            no = Cargo.objects.count()
            if no == 0:
                self.ID = 1
            else:
                self.ID = self.__class__.objects.all().order_by(
                    "-ID")[0].ID + 1

        super(Cargo, self).save(*args, **kwargs)

El cargo puede estar activo, o no. (Por X razón ya no está en uso, o cualquier otra cosa... Echadle imaginación ;-)

Por defecto, el widget listará todos los cargos disponibles.

Para realizar ambas cosas, debemos definir la función formfield_for_foreignkey en nuestro admin.py

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == 'cargo':
            kwargs['queryset'] = Cargo.objects.filter(activo='S').order_by('nombre')
        return super(ConsumerAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

En este caso, modificamos el queryset que mostrará el select "cargo" (esto se corresponde al field del model de Consumer definido como "cargo". Estamos filtrando que muestre únicamente los marcados como "activo", y los ordene por "nombre". Obteniendo como resultado:
Esto tiene un montón de posibilidades. Puedes hacer muchas otras cosas en e lqueryset del select. Invertir orden, etc.. Incluso ordenar por una foreignkey de esta foreignkey.

order_by('foreignKeyName__field')

Si te es útil, no dudes en compartirlo. Y si tienes dudas, pregunta :)

Saludos!
Alberto.