Date Conveniences (Validation, Display, etc) for Partial Dates in Django - django

I am trying to store dates in my database that often lack the month and day. My current approach (there seem to be many different ways of doing this) is to use three fields for this:
dob_year
dob_month
dob_day
I'd like to get as many of the benefits of DateFields that I can. The most important one I can think of now is validation. Valid:
2010
2010,02
2010,02,28
Invalid:
2010,02,30
There's also the problem of converting ints into a human readable form in the templates. It's great to be able to say:
<p>{{my_date|date:"%Y"}}</p>
But with this, I'll have to do something very strange because I'll need it to support partial dates and regular ones. I'm thinking I might be able to accomplish this with a #property method, but I haven't sorted this out yet either.
I'm sure there are other conveniences I'm giving up too (like date widgets in admin). Ideas here are welcome too since I know partial dates are a common issue, but I'm mostly concerned with validation at the moment.
Update: A friend on Twitter pointed out that using three fields for this creates horrible date queries. Do not use three fields for partial dates unless you want to think about how to handle queries like "between July of 2011 and June of 2012".

Store dates in database as text is a bad practice. It is not portable, not django query api friendly, and not index friendly.
You can store all date in database even not all date is needed. For example, Oracle stores date and time in dates even you only need the date.
Then you can use a DateField + "ChoiceField" to store partialdate and relevant part information. Ex: 2015-02-01 truncate at month level. 2015-02-28 Full date. 2015-01-01 truncate at year level. Code:
class Item(models.Model):
PARTIAL_YEAR='%Y'
PARTIAL_MONTH='%Y-%m'
PARTIAL_DAY='%Y-%m-%d'
PARTIAL_CHOICES = (
(PARTIAL_YEAR, 'Year'),
(PARTIAL_MONTH, 'Month'),
(PARTIAL_DAY, 'Day'),
)
partial_date_date = models.DateField()
partial_date_part = models.CharField('Date part',
choices=PARTIAL_CHOICES,
max_length=10, )
Validate Both fields will be validated by each own widget. You can add a form clean (Cleaning and validating fields that depend on each other) or model clean validation level.
Quering Easy to make conditioned queries using Q objects:
q_is_year = q( partial_date_part = Item.PARTIAL_YEAR )
q_by_year = q( partial_date_date__year = 2015 )
q_is_month = q( partial_date_part = Item.PARTIAL_MONTH )
q_by_month = q( partial_date_date__year = 2105 )
q_by_month &= q( partial_date_date__month = 2 )
qs = Item.objects.filter( q_is_year&q_by_year | q_is_month&q_by_month )
Render display In order to render in template:
<p>{{item.partial_date_date|date:item.partial_date_part}}</p>
Render form To render form controls you can use JavaScript to change UI and help user with data entry:
date type: (*) Year ( ) Month ( ) Day
date: [ change form widget dynamically ]
You can send 3 widgets controls to form and show just one at a time. Change visibility on changing radio date type selection. I use MultiWidget.
EDITED 13 August 2015 with all sample code:
models.py
from django.db import models
from datetime import date
class Item(models.Model):
PARTIAL_YEAR='%Y'
PARTIAL_MONTH='%Y-%m'
PARTIAL_DAY='%Y-%m-%d'
PARTIAL_CHOICES = (
(PARTIAL_YEAR, 'Year'),
(PARTIAL_MONTH, 'Month'),
(PARTIAL_DAY, 'Day'),
)
partial_date_part = models.CharField('Date part',
choices=PARTIAL_CHOICES,
max_length=10, )
partial_date_date = models.DateField()
some_comment = models.CharField('Comment', max_length=100, )
def save(self, *args, **kwargs):
if self.partial_date_part==self.PARTIAL_YEAR:
self.partial_date_date = date( self.partial_date_date.year, 1, 1 )
elif self.partial_date_part==self.PARTIAL_MONTH:
self.partial_date_date = date( self.partial_date_date.year,
self.partial_date_date.month, 1 )
super(Item, self).save(*args, **kwargs)
forms.py
from django import forms
from django.forms import widgets
from datetime import date
class DateSelectorWidget(widgets.MultiWidget):
def __init__(self, attrs=None):
days = [(d, d) for d in range(1,32)]
months = [(m, m) for m in range(1,13)]
years = [(year, year) for year in (2011, 2012, 2013)]
_widgets = (
widgets.Select(attrs=attrs, choices=days),
widgets.Select(attrs=attrs, choices=months),
widgets.Select(attrs=attrs, choices=years),
)
super(DateSelectorWidget, self).__init__(_widgets, attrs)
def decompress(self, value):
if value:
return [value.day, value.month, value.year]
return [None, None, None]
def format_output(self, rendered_widgets):
return ''.join(rendered_widgets)
def value_from_datadict(self, data, files, name):
datelist = [
widget.value_from_datadict(data, files, name + '_%s' % i)
for i, widget in enumerate(self.widgets)]
D = date(
day=int(datelist[0]),month=int(datelist[1]),year=int(datelist[2]),
)
return D
class ItemForm(forms.Form):
partial_date_part = forms.CharField(widget=forms.RadioSelect)
partial_date_date = DateSelectorWidget( )
view.py
from django.http import HttpResponseRedirect
from django.views.generic import View
from models import Item
from django.forms.models import modelform_factory
from .forms import DateSelectorWidget
from django import forms
from django.forms import widgets
class MyDatAppView(View):
form_class = modelform_factory(Item ,
exclude=[],
widgets={ 'partial_date_date':
DateSelectorWidget() ,})
initial = {'some_comment': '-*-', }
template_name = 'form.html'
def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request, self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
m=form.save()
return HttpResponseRedirect('/')
return render(request, self.template_name, {'form': form})
You should add javascript on template to hide/show date part fields when selected part changes.

Related

Add 'yesterday' field to Django admin date list filter

In my Django application, in the admin, for one of my models, I am allowing the option to filter by its 'create_date' field. Django by default gives me some options (Today, Past 7 Days, This Month, This Year). I want to simply add the option to choose 'Yesterday' as well. I looked at other Stack overflow questions regarding the same issue, but they were all looking for the ability to search by a date range, and I only want the one preloaded option. Is their a way in the admin class that configures this model to override some of their filter functionality ?
Admin Class
class User_LikeAdmin(admin.ModelAdmin):
def fb_view_link(self, obj):
if len(obj.user_facebook_link) > 2:
return u"<a href='%s' target='_blank'>Facebook Page</a>" % obj.user_facebook_link
else:
return ""
fb_view_link.short_description = ''
fb_view_link.allow_tags = True
list_display = ('vehicle', 'user', 'fb_view_link', 'dealer', 'create_date')
list_filter = ('create_date', ('vehicle__dealer', custom_titled_filter('Dealer')))
raw_id_fields = ('vehicle', 'user')
actions = [export_csv]
def dealer(self, obj):
return obj.vehicle.dealer
As an option, you can use custom filter class as mentioned in the documentation
class User_LikeAdmin(admin.ModelAdmin):
list_filter = (('create_date', CustomDateFieldListFilter),)
You can extend DateFieldListFilter
from django.contrib.admin.filters import DateFieldListFilter
class CustomDateFieldListFilter(DateFieldListFilter):
# Your tweaks here
import datetime
from django.contrib.admin import DateFieldListFilter
class DateYesterdayFieldListFilter(DateFieldListFilter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
self.links = list(self.links)
self.links.insert(2, ('Yesterday', {
self.lookup_kwarg_since: str(yesterday),
self.lookup_kwarg_until: str(today),
}))
Use the datetime, and create yesterday variable, and then get all records this way.
import datetime
yesterday = datetime.date.today() - datetime.timedelta(days=1)
data = Modelname.objects.filter(create_date=yesterday)
My version of #d2718nis answer:
class DateFilter(DateFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
now = timezone.now()
if timezone.is_aware(now):
now = timezone.localtime(now)
if isinstance(field, models.DateTimeField):
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
else: # field is a models.DateField
today = now.date()
yesterday = today - datetime.timedelta(days=1)
links_start = self.links[:2] # anyday and today
link_yesterday = (_('Yesterday'), {
self.lookup_kwarg_since: str(yesterday),
self.lookup_kwarg_until: str(today),
})
self.links = (*links_start, link_yesterday, *self.links[2:])

Django - Costum admin filter not by field [duplicate]

How can I add a custom filter to django admin (the filters that appear on the right side of a model dashboard)? I know its easy to include a filter based on a field of that model, but what about a "calculated" field like this:
class NewsItem(models.Model):
headline = models.CharField(max_length=4096, blank=False)
byline_1 = models.CharField(max_length=4096, blank=True)
dateline = models.DateTimeField(help_text=_("date/time that appears on article"))
body_copy = models.TextField(blank=False)
when_to_publish = models.DateTimeField(verbose_name="When to publish", blank=True, null=True)
# HOW CAN I HAVE "is_live" as part of the admin filter? It's a calculated state!!
def is_live(self):
if self.when_to_publish is not None:
if ( self.when_to_publish < datetime.now() ):
return """ <img alt="True" src="/media/img/admin/icon-yes.gif"/> """
else:
return """ <img alt="False" src="/media/img/admin/icon-no.gif"/> """
is_live.allow_tags = True
class NewsItemAdmin(admin.ModelAdmin):
form = NewsItemAdminForm
list_display = ('headline', 'id', 'is_live')
list_filter = ('is_live') # how can i make this work??
Thanks to gpilotino for giving me the push into the right direction for implementing this.
I noticed the question's code is using a datetime to figure out when its live . So I used the DateFieldFilterSpec and subclassed it.
from django.db import models
from django.contrib.admin.filterspecs import FilterSpec, ChoicesFilterSpec,DateFieldFilterSpec
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext as _
from datetime import datetime
class IsLiveFilterSpec(DateFieldFilterSpec):
"""
Adds filtering by future and previous values in the admin
filter sidebar. Set the is_live_filter filter in the model field attribute
'is_live_filter'. my_model_field.is_live_filter = True
"""
def __init__(self, f, request, params, model, model_admin):
super(IsLiveFilterSpec, self).__init__(f, request, params, model,
model_admin)
today = datetime.now()
self.links = (
(_('Any'), {}),
(_('Yes'), {'%s__lte' % self.field.name: str(today),
}),
(_('No'), {'%s__gte' % self.field.name: str(today),
}),
)
def title(self):
return "Is Live"
# registering the filter
FilterSpec.filter_specs.insert(0, (lambda f: getattr(f, 'is_live_filter', False),
IsLiveFilterSpec))
To use you can put the above code into a filters.py, and import it in the model you want to add the filter to
you have to write a custom FilterSpec (not documentend anywhere).
Look here for an example:
http://www.djangosnippets.org/snippets/1051/
In current django development version there is the support for custom filters: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter
You can't, unfortunately. Currently non-field items can not be used as list_filter entries.
Note that your admin class wouldn't have worked even if it was a field, as a single-item tuple needs a comma: ('is_live',)
Just a sidenote: You can use the deafult ticks on Django admin more easily like this:
def is_live(self):
if self.when_to_publish is not None:
if ( self.when_to_publish < datetime.now() ):
return True
else:
return False
is_live.boolean = True
Not an optimal way (CPU-wise) but simple and will work, so I do it this way (for my small database). My Django version is 1.6.
In admin.py:
class IsLiveFilter(admin.SimpleListFilter):
title = 'Live'
parameter_name = 'islive'
def lookups(self, request, model_admin):
return (
('1', 'islive'),
)
def queryset(self, request, queryset):
if self.value():
array = []
for element in queryset:
if element.is_live.__call__() == True:
q_array.append(element.id)
return queryset.filter(pk__in=q_array)
...
class NewsItemAdmin(admin.ModelAdmin):
form = NewsItemAdminForm
list_display = ('headline', 'id', 'is_live')
list_filter = (IsLiveFilter)
Key idea here is to access custom fields in a QuerySet via __call__() function.
The user supplies goods to some countries postage free. I wanted to filter those countries:
All - all countries, Yes - postage free, No - charged postage.
The main answer for this question did not work for me (Django 1.3) I think because there was no field_path parameter provided in the __init__ method. Also it subclassed DateFieldFilterSpec. The postage field is a FloatField
from django.contrib.admin.filterspecs import FilterSpec
class IsFreePostage(FilterSpec):
def __init__(self, f, request, params, model, model_admin, field_path=None):
super(IsFreePostage, self).__init__(f, request, params, model,
model_admin, field_path)
self.removes = {
'Yes': ['postage__gt'],
'No': ['postage__exact'],
'All': ['postage__exact', 'postage__gt'] }
self.links = (
('All', {}),
('Yes', {'postage__exact': 0}),
('No', {'postage__gt': 0}))
if request.GET.has_key('postage__exact'):
self.ttl = 'Yes'
elif request.GET.has_key('postage__gt'):
self.ttl = 'No'
else:
self.ttl = 'All'
def choices(self, cl):
for title, param_dict in self.links:
yield {'selected': title == self.ttl,
'query_string': cl.get_query_string(param_dict,
self.removes[title]),
'display': title}
def title(self):
return 'Free Postage'
FilterSpec.filter_specs.insert(0,
(lambda f: getattr(f, 'free_postage', False), IsFreePostage))
In self.links we supply dicts. used to construct HTTP query strings like ?postage__exact=0 for each of the possible filters. Filters I think are cumulative so if there was a previous request for 'No' and now we have a request for 'Yes' we have to remove the
'No' query. self.removes specifies what needs to be removed for each query. The choices method constructs the query strings, says which filter has been selected and sets the displayed name of the filter.
Here is the answer and implemented the custom filter as simple as possible this might help
Django admin date range filter

Custom Filter for Date Field in Django Admin, Django 1.2

Is this still valid syntax for Django 1.2?
Custom Filter in Django Admin on Django 1.3 or below
I have tried it, but the list_filter option in the admin class is not recognizing my custom filter.
How should the custom filter be added to the list_filter so that it displays?
class MyModelAdmin(admin.ModelAdmin):
...
list_filter = ['is_expired_filter']
Here my 'is_expired_filter' is my newly registered custom filter, which is what the Author says he does like so:
list_filter = ('is_live')
But this is not recognized by Django, and the error I get when I load the admin page is
Exception Type: ImproperlyConfigured
Exception Value: 'PositionAdmin.list_filter[2]' refers to field 'is_expired_filter' that is missing from model 'Position'
Perhaps my mistake is that I am not sure how the original code is used by the Author of that question once he/she implements a custom filter.
Here is the original code:
def is_live(self):
if self.when_to_publish is not None:
if ( self.when_to_publish < datetime.now() ):
return """ <img alt="True" src="/media/img/admin/icon-yes.gif"/> """
else:
return """ <img alt="False" src="/media/img/admin/icon-no.gif"/> """
is_live.allow_tags = True
Now that I have a handle on what I think you want, I'm assuming you have a model that you want to filter by a DateField like:
class Position(models.Model):
expiration_date = models.DateField()
...
which you should now modify to
class Position(models.Model):
expiration_date = models.DateField()
expiration_date.is_expired_filter = True
...
What you want to do is add to your admin.py a new filter class
from django.contrib.admin.filterspecs import FilterSpec, DateFieldFilterSpec
from django.utils.translation import ugettext as _
from datetime import datetime, date
class ExpiredFilterSpec(DateFieldFilterSpec):
"""
Adds filtering by future and previous values in the admin
filter sidebar. Set the is_expired_filter filter in the model field
attribute 'is_expired_filter'.
my_model_field.is_expired_filter = True
"""
def __init__(self, f, request, params, model, model_admin, **kwargs):
super(ExpiredFilterSpec, self).__init__(f, request, params, model,
model_admin, **kwargs)
today = date.today()
self.links = (
(_('All'), {}),
(_('Not Expired'), {'%s__lt' % self.field.name: str(today),
}),
(_('Expired'), {'%s__gte' % self.field.name: str(today),
}))
def title(self):
return "Filter By Expiration Date"
# registering the filter
FilterSpec.filter_specs.insert(0, (lambda f: getattr(f, 'is_expired_filter', False),
ExpiredFilterSpec))
class PositionAdmin(admin.ModelAdmin):
list_filter = ['expiration_date']
Almost copying your link Custom Filter in Django Admin on Django 1.3 or below word for word, I came up with this.
from django.contrib.admin.filterspecs import FilterSpec, ChoicesFilterSpec, DateFieldFilterSpec
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext as _
from datetime import datetime
class IsExpiredFilterSpec(DateFieldFilterSpec):
"""
Adds filtering by future and previous values in the admin
filter sidebar. Set the is_expired_filter filter in the model field
attribute 'is_expired_filter'.
my_model_field.is_expired_filter = True
"""
def __init__(self, f, request, params, model, model_admin):
super(IsExpiredFilterSpec, self).__init__(f, request, params, model,
model_admin)
# -- You'll need to edit this to make it do what you want. --
# today = datetime.now()
# self.links = (
# (_('Any'), {}),
# (_('Yes'), {'%s__lte' % self.field.name: str(today),
# }),
# (_('No'), {'%s__gte' % self.field.name: str(today),
# }),
#
# )
def title(self):
return "Is Expired"
\# registering the filter
FilterSpec.filter_specs.insert(0, (lambda f: getattr(f, 'is_expired_filter', False),
IsExpiredFilterSpec))
class MyModelAdmin(admin.ModelAdmin):
...
MODEL_FIELD_TO_FILTER.is_expired_filter = True
list_filters = ['MODEL_FIELD_TO_FILTER']
UPDATE: Made a change thanks to jimbob. MODEL_FIELD_TO_FILTER would be the field you want to filter.

Django: How to make a query for on object based on an M2M field (multiple selections for field on search form)

I need help coming up with an efficient way to do a search query for a set of objects, based on a M2M field. My search form is going to look something like Blue Cross Blue Shield's | eg: this image
Now, let's suppose my model looks like this:
# models.py
class Provider(models.Model)
title = models.CharField(max_length=150)
phone = PhoneNumberField()
services_offered = models.ManyToManyField(ServiceType)
def __unicode__(self):
return self.title
class ServiceCategory(models.Model):
service_category = models.CharField(max_length=30)
def __unicode__(self):
return self.service_category
class Meta(object):
verbose_name_plural = "Service Categories"
class ServiceType(models.Model):
service_type = models.CharField(max_length=30)
service_category = models.ForeignKey(ServiceCategory)
def __unicode__(self):
return u'%s | %s' % (self.service_category, self.service_type
Also, we have to keep in mind that the options that we select are subject to change, since how they display on the form is dynamic (new ServiceCategories and ServiceTypes can be added at anytime). *How should I go about constructing a query for the Provider objects, given that a person using the search form can select multiple Services_Offered?*
This is currently my HIGHLY INEFFICIENT METHOD:
#managers.py
from health.providers.models import *
from django.db.models import Q
class Query:
def __init__(self):
self.provider_objects=Provider.objects.all()
self.provider_object=Provider.objects
self.service_object=ServiceType.objects
self.category_objects=ServiceCategory.objects.all()
def simple_search_Q(self, **kwargs): #matt's learning note: **kwargs passes any dictionary
return self.provider_objects.filter(
Q(services_offered__service_type__icontains=kwargs['service']),
Q(title__icontains=kwargs['title']),
Q(state=kwargs['state']),
).distinct().order_by('title')
====================
#views.py
from django.shortcuts import render_to_response
from health.providers.models import *
from health.search.forms import *
from health.search.managers import Query #location of the query sets
from django.core.paginator import Paginator, InvalidPage, EmptyPage
from django.template import RequestContext
def simple_search(request):
if request.method == 'POST':
SimpleSearch_form = SimpleSearch(request.POST)
if SimpleSearch_form.is_valid():
request.session["provider_list"] = None
kwargs = {'title': request.POST['title'],
'service': request.POST['service'], 'state': request.POST['state'] }
provider_list = Query().simple_search_Q(**kwargs)
return pagination_results(request, provider_list)
else:
SimpleSearch_form = SimpleSearch()
return render_to_response('../templates/index.html', { 'SimpleSearch_form': SimpleSearch_form},
context_instance=RequestContext(request))
How can I make my query:
Obtain Provider objects based on selecting multiple request.POST['service']
More efficient
Thanks for any help in advanced.
Best Regards,
Matt
1: for multiple request.POST['service'], I assume you mean these are CheckBoxes.
I'd make the CheckBox values ID's, not names, and do a PK lookup.
'services_offered__pk__in': request.POST.getlist('service')
That would return all Provider objects that have ALL of the services selected.
PS: You are also using CapitalCase for instances which is very confusing. If you want your code to be readable, I highly recommend some changes to your style (don't use CapitalCase for instances or variables) and make your variables more descriptive.
SimpleSearch_form = SimpleSearch() # what is SimpleSearch?
simplesearch_form = SimpleSearchForm() # now, it's very clear what the class SimpleSearchForm is
# and the form instance is clearly a for instance.
2: making it more efficient? You could get rid of a lot of code and code separation by remove your whole Query class. Also, I don't know why you are using Q objects since you are not doing anything that would require it (like OR or OR + AND).
def simple_search(request):
if request.method == 'POST':
searchform = SimpleSearchForm(request.POST)
if searchform.is_valid():
request.session['provider_list'] = None
post = request.POST
providers = Provider.objects.filter(services_offered__pk__in=post.getlist('services'),
title=post['title'], state=post['state'])
return pagination_results(request, provider_list)
else:
searchform = SimpleSearchForm()
return direct_to_template(request, '../templates/index.html', { 'searchform': searchform})

Django admin filter using F() expressions

does someone know how to filter in admin based on comparison on model fields - F() expressions?
Let's assume we have following model:
class Transport(models.Model):
start_area = models.ForeignKey(Area, related_name='starting_transports')
finish_area = models.ForeignKey(Area, related_name='finishing_transports')
Now, what I would like to do is to make admin filter which allows for filtering of in-area and trans-area objects, where in-area are those, whose start_area and finish_area are the same and trans-area are the others.
I have tried to accomplish this by creating custom FilterSpec but there are two problems:
FilterSpec is bound to only one field.
FilterSpec doesn't support F() expressions and exclude.
The second problem might be solved by defining custom ChangeList class, but I see no way to solve the first one.
I also tried to "emulate" the filter straight in the ModelAdmin instance by overloading queryset method and sending extra context to the changelist template where the filter itself would be hard-coded and printed by hand. Unfortunately, there seems to be problem, that Django takes out my GET parameters (used in filter link) as they are unknown to the ModelAdmin instance and instead, it puts only ?e=1 which is supposed to signal some error.
Thanks anyone in advance.
EDIT: It seems that functionality, which would allow for this is planned for next Django release, see http://code.djangoproject.com/ticket/5833. Still, does someone have a clue how to accomplish that in Django 1.2?
it's not the best way*, but it should work
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
The solution involves adding your FilterSpec and as you said implementing your own ChangeList. As the filter name is validated, you must name your filter with a model field name. Below you will see a hack allowing to use the default filter for the same field.
You add your FilterSpec before the standard FilterSpecs.
Below is a working implementation running on 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)
Unfortunately, FilterSpecs are very limited currently in Django. Simply, they weren't created with customization in mind.
Thankfully, though, many have been working on a patch to FilterSpec for a long time. It missed the 1.3 milestone, but it looks like it's now finally in trunk, and should hit with the next release.
#5833 (Custom FilterSpecs)
If you want to run your project on trunk, you can take advantage of it now, or you might be able to patch your current installation. Otherwise, you'll have to wait, but at least it's coming soon.