2010-11-24 13 views
12

¿Alguien sabe cómo filtrar en administrador basado en la comparación en campos modelo - expresiones F()?Filtro Django admin usando expresiones F()

Supongamos que hemos siguiente modelo:

class Transport(models.Model): 
    start_area = models.ForeignKey(Area, related_name='starting_transports') 
    finish_area = models.ForeignKey(Area, related_name='finishing_transports') 

Ahora, lo que me gustaría hacer es hacer que el filtro de administración que permite la filtración de dentro del área y trans del área de objetos, donde dentro del área y están aquellos, cuyos start_area y finish_area son los mismos y trans-area son los otros.

He tratado de lograr esto mediante la creación personalizada Filterspec pero hay dos problemas:

  • Filterspec está unido a un solo campo.
  • FilterSpec no admite expresiones F() y excluye.

El segundo problema se puede resolver definiendo una clase de Lista de cambios personalizada, pero no veo forma de resolver la primera.

También traté de "emular" el filtro directamente en la instancia de ModelAdmin al sobrecargar el método queryset y enviar contexto adicional a la plantilla de la lista de cambios donde el filtro se codificaría e imprimiría a mano. Lamentablemente, parece haber un problema, que Django elimina mis parámetros GET (utilizados en el enlace del filtro) ya que son desconocidos para la instancia de ModelAdmin y, en su lugar, solo pone? E = 1, que se supone que indica algún error.

Gracias a nadie de antemano.

EDIT: Parece que la funcionalidad, que permitiría esto está prevista para la próxima versión de Django, véase http://code.djangoproject.com/ticket/5833. Aún así, ¿alguien tiene una idea de cómo lograr eso en Django 1.2?

+0

¿Qué es 'trans-area'? :-) –

+0

Nada especial;) Se supone que son todas las instancias del objeto Transport en databse cuyo start_area es diferente de finish_area. – xaralis

+1

Bueno, parece que nadie tiene la respuesta. La única solución que se me ocurrió hasta ahora es muy fea: agregue otro campo al modelo de Transporte que se actualizará en guardar para guardar información si el transporte está en área o área trans :(Odio esos campos redundantes :( – xaralis

Respuesta

1

La solución consiste en agregar su FilterSpec y, como dijo, implementar su propia ChangeList. Como el nombre del filtro está validado, debe nombrar su filtro con un nombre de campo modelo. A continuación, verá un truco que permite usar el filtro predeterminado para el mismo campo.

Añade su FilterSpec antes de FilterSpecs estándar.

A continuación se muestra una implementación en funcionamiento que se ejecuta en Django 1.3

from django.contrib.admin.views.main import * 
from django.contrib import admin 
from django.db.models.fields import Field 
from django.contrib.admin.filterspecs import FilterSpec 
from django.db.models import F 
from models import Transport, Area 
from django.contrib.admin.util import get_fields_from_path 
from django.utils.translation import ugettext as _ 


# Our filter spec 
class InAreaFilterSpec(FilterSpec): 

    def __init__(self, f, request, params, model, model_admin, field_path=None): 
     super(InAreaFilterSpec, self).__init__(
      f, request, params, model, model_admin, field_path=field_path) 
     self.lookup_val = request.GET.get('in_area', None) 

    def title(self): 
     return 'Area' 

    def choices(self, cl): 
     del self.field._in_area 
     yield {'selected': self.lookup_val is None, 
       'query_string': cl.get_query_string({}, ['in_area']), 
       'display': _('All')} 
     for pk_val, val in (('1', 'In Area'), ('0', 'Trans Area')): 
      yield {'selected': self.lookup_val == pk_val, 
        'query_string': cl.get_query_string({'in_area' : pk_val}), 
        'display': val} 

    def filter(self, params, qs): 
     if 'in_area' in params: 
      if params['in_area'] == '1': 
       qs = qs.filter(start_area=F('finish_area')) 
      else: 
       qs = qs.exclude(start_area=F('finish_area')) 
      del params['in_area'] 
     return qs 

def in_area_test(field): 
    # doing this so standard filters can be added with the same name 
    if field.name == 'start_area' and not hasattr(field, '_in_area'): 
     field._in_area = True 
     return True  
    return False 

# we add our special filter before standard ones 
FilterSpec.filter_specs.insert(0, (in_area_test, InAreaFilterSpec)) 


# Defining my own change list for transport 
class TransportChangeList(ChangeList): 

    # Here we are doing our own initialization so the filters 
    # are initialized when we request the data 
    def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin): 
     #super(TransportChangeList, self).__init__(request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin) 
     self.model = model 
     self.opts = model._meta 
     self.lookup_opts = self.opts 
     self.root_query_set = model_admin.queryset(request) 
     self.list_display = list_display 
     self.list_display_links = list_display_links 
     self.list_filter = list_filter 
     self.date_hierarchy = date_hierarchy 
     self.search_fields = search_fields 
     self.list_select_related = list_select_related 
     self.list_per_page = list_per_page 
     self.model_admin = model_admin 

     # Get search parameters from the query string. 
     try: 
      self.page_num = int(request.GET.get(PAGE_VAR, 0)) 
     except ValueError: 
      self.page_num = 0 
     self.show_all = ALL_VAR in request.GET 
     self.is_popup = IS_POPUP_VAR in request.GET 
     self.to_field = request.GET.get(TO_FIELD_VAR) 
     self.params = dict(request.GET.items()) 
     if PAGE_VAR in self.params: 
      del self.params[PAGE_VAR] 
     if TO_FIELD_VAR in self.params: 
      del self.params[TO_FIELD_VAR] 
     if ERROR_FLAG in self.params: 
      del self.params[ERROR_FLAG] 

     if self.is_popup: 
      self.list_editable =() 
     else: 
      self.list_editable = list_editable 
     self.order_field, self.order_type = self.get_ordering() 
     self.query = request.GET.get(SEARCH_VAR, '') 
     self.filter_specs, self.has_filters = self.get_filters(request) 
     self.query_set = self.get_query_set() 
     self.get_results(request) 
     self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name)) 
     self.pk_attname = self.lookup_opts.pk.attname 


    # To be able to do our own filter, 
    # we need to override this 
    def get_query_set(self): 

     qs = self.root_query_set 
     params = self.params.copy() 

     # now we pass the parameters and the query set 
     # to each filter spec that may change it 
     # The filter MUST delete a parameter that it uses 
     if self.has_filters: 
      for filter_spec in self.filter_specs: 
       if hasattr(filter_spec, 'filter'): 
        qs = filter_spec.filter(params, qs) 

     # Now we call the parent get_query_set() 
     # method to apply subsequent filters 
     sav_qs = self.root_query_set 
     sav_params = self.params 

     self.root_query_set = qs 
     self.params = params 

     qs = super(TransportChangeList, self).get_query_set() 

     self.root_query_set = sav_qs 
     self.params = sav_params 

     return qs 


class TransportAdmin(admin.ModelAdmin): 
    list_filter = ('start_area','start_area') 

    def get_changelist(self, request, **kwargs): 
     """ 
     Overriden from ModelAdmin 
     """ 
     return TransportChangeList 


admin.site.register(Transport, TransportAdmin) 
admin.site.register(Area) 
+0

Gracias por esto. Desafortunadamente, esto parece tan innecesario que preferiría elegir la forma de agregar un campo redundante. Afortunadamente, los desarrolladores de django arreglarán esto en la próxima versión (http://code.djangoproject.com/ticket/5833). – xaralis

3

que no es la mejor manera *, pero debería funcionar

class TransportForm(forms.ModelForm): 
    transports = Transport.objects.all() 
    list = [] 
    for t in transports: 
     if t.start_area.pk == t.finish_area.pk: 
      list.append(t.pk) 
    select = forms.ModelChoiceField(queryset=Page.objects.filter(pk__in=list)) 

    class Meta: 
     model = Transport 
+0

Sí esto parece la solución – KronnorK

0

Desafortunadamente, FilterSpecs son muy limitadas actualmente en Django. Simplemente, no fueron creados con personalización en mente.

Afortunadamente, sin embargo, muchos han estado trabajando en un parche para FilterSpec durante mucho tiempo. Se perdió el hito de 1.3, pero parece que ahora finalmente está en el maletero y debería golpear con el siguiente lanzamiento.

#5833 (Custom FilterSpecs)

Si desea ejecutar su proyecto en el tronco, que puede tomar ventaja de ella ahora, o usted podría ser capaz de arreglar su instalación actual. De lo contrario, tendrás que esperar, pero al menos llegará pronto.