The following code is working nicely:
class SelectTwoTeams(forms.Form):
campaignnoquery = UserSelection.objects.filter(user=349).order_by('-campaignno')[:1]
currentCampaignNo = campaignnoquery[0].campaignno
cantSelectTeams = UserSelection.objects.filter(campaignno=currentCampaignNo)
currentTeams = StraightredTeam.objects.filter(currentteam = 1).exclude(teamid__in=cantSelectTeams.values_list('teamselectionid', flat=True))
team_one = forms.ModelChoiceField(queryset = currentTeams)
team_two = forms.ModelChoiceField(queryset = currentTeams)
However, you can see that the user id is currently hardcoded into the filter as 349. I would like this to be the id of the user logged in. I know in the view I can use:
currentUser = request.user
currentUserID = currentUser.id
But this code does not work within the forms section. If anyone could point me in the correct direction that would be ideal.
When I follow the suggestion below using the following form I get an error saying: NameError: name 'currentUserID' is not defined
# coding=utf-8
from dwad.threadlocals import get_current_user
from django.db.models import Max
from django import forms
from straightred.models import StraightredTeam
from straightred.models import UserSelection
class SelectTwoTeams(forms.Form):
def save(self):
currentUser = get_current_user()
currentUserID = currentUser.id
campaignnoquery = UserSelection.objects.filter(user=currentUserID).order_by('-campaignno')[:1]
currentCampaignNo = campaignnoquery[0].campaignno
cantSelectTeams = UserSelection.objects.filter(campaignno=currentCampaignNo)
currentTeams = StraightredTeam.objects.filter(currentteam = 1).exclude(teamid__in=cantSelectTeams.values_list('teamselectionid', flat=True))
team_one = forms.ModelChoiceField(queryset = currentTeams)
team_two = forms.ModelChoiceField(queryset = currentTeams)
Many thanks, Alan.
One method is to use local.threading. I have used this solution on a number of Django installations to good use.
I know there are a number of different opinions whether this is a good or bad solution. I tend to fall into the category that it can be extremely good in the right circumstances.
To set it up, create a file called threadlocals.py:
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
_thread_locals = local()
def get_current_user():
return getattr(_thread_locals, 'user', None)
class ThreadLocalsMiddleware(object):
def process_request(self, request):
_thread_locals.user = getattr(request, 'user', None)
Then, add this ThreadLocalsMiddleware class to your project's middleware in settings.py:
MIDDLEWARE_CLASSES = [
...
'myproject.threadlocals.ThreadLocalsMiddleware',
...
]
Now, all you need to do is call the method get_current_user() from anywhere in your project.
from myproject.threadlocals import get_current_user
class SelectTwoTeams(forms.Form):
def save(self):
# for example:
currentUser = get_current_user()
currentUserID = currentUser.id
Found this answer at Reddit.
It is very simple and it is working good in my case.
In your view you have to include some code like this:
if form.is_valid():
obj = form.save(commit=False)
obj.user = request.user # logged in user is available on a view func's `request` instance
obj.save() # safe to save w/ user in tow
Related
I have a createview view in my django app:
### Create a Group
class GroupCreateView(CreateView): # {{{
model = Group
form_class = GroupForm
template_name = 'ipaswdb/group/group_form.html'
success_url = '/ipaswdb/group/'
def get_context_data(self, **kwargs):
..do stuff..
def post(self, request, *args, **kwargs):
if self.request.POST.has_key('submit'):
form = GroupForm(request.POST)
if form.is_valid():
### Save the group
self.object = form.save()
#### Adding a provider forces a default location
#if form['default_location'].value() == True:
### We are forcing the creation of a GroupLocation when a new Group is created
gl = GroupLocation(
group = Group.objects.get(pk=self.object.id),
doing_business_as = self.object.group_name,
default_group_location = True,
mailing_address_line_one = self.object.mailing_address_line_one,
mailing_address_line_two = "",
mailing_city = self.object.mailing_city,
mailing_state = self.object.mailing_state,
mailing_zip_code = self.object.mailing_zip_code,
mailing_phone = self.object.mailing_phone,
mailing_fax = self.object.mailing_fax,
contact = self.object.group_contact,
physical_address_line_one = self.object.billing_address_line_one,
physical_address_line_two = "",
physical_city = self.object.billing_city,
physical_state = self.object.billing_state,
physical_zip_code = self.object.billing_zip_code,
physical_phone = self.object.billing_phone,
physical_fax = self.object.billing_fax,
)
gl.save()
new_grploc = gl
self.object.default_location_id = new_grploc.id
self.object.save()
new_group_id = self.object.id
new_grploc_id = new_grploc.id
### Now check the list of providers to see what has changed
print "group_add_provider: ",
print request.POST.getlist('group_add_provider')
add_providers = request.POST.getlist('group_add_provider')
if add_providers:
for pro in add_providers:
add_grploc = GroupLocationProvider(
grouplocation=GroupLocation.objects.get(pk=new_grploc_id),
provider=Provider.objects.get(pk=pro)
)
add_grploc.save()
### Now check the list of insurances to see what has changed
print "group_add_insurance: ",
print request.POST.getlist('group_add_insurance')
add_insurances = request.POST.getlist('group_add_insurance')
if add_insurances:
for ins in add_insurances:
add_grpins = GroupInsurance(
group=Group.objects.get(pk=new_group_id),
insurance=Insurance.objects.get(pk=ins)
)
add_grpins.save()
#return HttpResponseRedirect(self.get_success_url()) #how it used to work, just fine but would go back to my list of groups
return HttpResponseRedirect('ipaswdb:group_detail', self.object.pk) #want it to call my edit view here.
My Url Patterns
app_name = 'ipaswdb'
urlpatterns = [
url(r'^group/(?P<pk>[0-9]+)/$', GroupUpdateView.as_view(), name='group_detail'),
url(r'^group/add/$', GroupCreateView.as_view(), name='group_add'),
..etc..
Got an error but I feel I am closer?
DisallowedRedirect at /ipaswdb/group/add/
Unsafe redirect to URL with protocol 'ipaswdb'
I really want to load the page with the created object but as an updateview
Anyway to do this from the create view?
It is highly recommended to return a redirect request from a successful POST request. Otherwise a user might accidentally create multiple objects by reloading the page. Something like this:
from django.shortcuts import redirect
...
return redirect('name-of-update-url', pk=obj.pk)
If you really do not want to use a redirect, it is a bit more involved. Class based views are not meant to be called directly. The as_view method you use in your urls.py creates a wrapper function which instantiates the class and calls dispatch, which selects the right handler method (get/post/...). But you can't use as_view, because you have a POST request, but probably want to call the get method.
So you have to create an instance of your UpdateView and directly call its get method. With a standard UpdateView, can try something like this:
class GroupCreateView(CreateView):
...
def post(self, request, *args, **kwargs):
...
obj = ... # create your object
update_view = UpdateView()
update_view.request = self.request
update_view.args = []
update_view.kwargs = {'pk': obj.pk}
return update_view.get(self.request)
If you heavily customized your UpdateView, you might have to adapt this.
My go-to resource how Django's class-based views look under the hood is https://ccbv.co.uk
Django-cms-saq is tested for 2.4.x. I'm trying to update the program to work with 3.0.X.
So far, I've updated all the imports but am coming across an unusual error. When I add a question (a plugin) to a page and hit publish, it creates two copies of the question in the database (viewable through the admin site). Deleting either copy removes both from the published page but leaves the question in edit mode.
How would I go about trouble shooting this?
I will include some of the files here. Please let me know if you need any other files.
Note that I am trying to add a multiple-choice question.
From models.py:
from django.db import models
from django.db.models import Max, Sum
from cms.models import CMSPlugin, Page, Placeholder
from cms.models.fields import PageField
from taggit.managers import TaggableManager
from djangocms_text_ckeditor.models import AbstractText
...
class Question(CMSPlugin):
QUESTION_TYPES = [
('S', 'Single-choice question'),
('M', 'Multi-choice question'),
('F', 'Free-text question'),
]
slug = models.SlugField(
help_text="A slug for identifying answers to this specific question "
"(allows multiple only for multiple languages)")
tags = TaggableManager(blank=True)
label = models.CharField(max_length=512, blank=True)
help_text = models.CharField(max_length=512, blank=True)
question_type = models.CharField(max_length=1, choices=QUESTION_TYPES)
optional = models.BooleanField(
default=False,
help_text="Only applies to free text questions",
)
depends_on_answer = models.ForeignKey(
Answer, null=True, blank=True, related_name='trigger_questions')
def copy_relations(self, oldinstance):
for answer in oldinstance.answers.all():
answer.pk = None
answer.question = self
answer.save()
self.depends_on_answer = oldinstance.depends_on_answer
#staticmethod
def all_in_tree(page):
root = page.get_root()
# Remember that there might be questions on the root page as well!
tree = root.get_descendants() | Page.objects.filter(id=root.id)
placeholders = Placeholder.objects.filter(page__in=tree)
return Question.objects.filter(placeholder__in=placeholders)
#staticmethod
def all_in_page(page):
placeholders = Placeholder.objects.filter(page=page)
return Question.objects.filter(placeholder__in=placeholders)
def score(self, answers):
if self.question_type == 'F':
return 0
elif self.question_type == 'S':
return self.answers.get(slug=answers).score
elif self.question_type == 'M':
answers_list = answers.split(',')
return sum([self.answers.get(slug=a).score for a in answers_list])
#property
def max_score(self):
if not hasattr(self, '_max_score'):
if self.question_type == "S":
self._max_score = self.answers.aggregate(
Max('score'))['score__max']
elif self.question_type == "M":
self._max_score = self.answers.aggregate(
Sum('score'))['score__sum']
else:
self._max_score = None # don't score free-text answers
return self._max_score
def percent_score_for_user(self, user):
if self.max_score:
try:
score = Submission.objects.get(
question=self.slug,
user=user,
).score
except Submission.DoesNotExist:
return 0
return 100.0 * score / self.max_score
else:
return None
def __unicode__(self):
return self.slug
...
From cms_plugins.py
import itertools
import operator
from django.contrib import admin
from django.utils.translation import ugettext as _
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from cms_saq.models import Question, Answer, GroupedAnswer, Submission, \
FormNav, ProgressBar, SectionedScoring, ScoreSection, BulkAnswer, \
QuestionnaireText, SubmissionSetReview
from djangocms_text_ckeditor.cms_plugins import TextPlugin
from djangocms_text_ckeditor.models import Text
from bs4 import BeautifulSoup
...
class QuestionPlugin(CMSPluginBase):
model = Question
module = "SAQ"
inlines = [AnswerAdmin]
exclude = ('question_type',)
def render(self, context, instance, placeholder):
user = context['request'].user
submission_set = None
triggered = True
depends_on = None
if instance.depends_on_answer:
depends_on = instance.depends_on_answer.pk
try:
Submission.objects.get(
user=user,
question=instance.depends_on_answer.question.slug,
answer=instance.depends_on_answer.slug,
submission_set=submission_set,
)
triggered = True
except:
triggered = False
extra = {
'question': instance,
'answers': instance.answers.all(),
'triggered': triggered,
'depends_on': depends_on,
}
if user.is_authenticated():
try:
extra['submission'] = Submission.objects.get(
user=user,
question=instance.slug,
submission_set=submission_set,
)
except Submission.DoesNotExist:
pass
context.update(extra)
return context
def save_model(self, request, obj, form, change):
obj.question_type = self.question_type
super(QuestionPlugin, self).save_model(request, obj, form, change)
...
class MultiChoiceQuestionPlugin(QuestionPlugin):
name = "Multi Choice Question"
render_template = "cms_saq/multi_choice_question.html"
question_type = "M"
exclude = ('question_type', 'help_text')
...
plugin_pool.register_plugin(SingleChoiceQuestionPlugin)
plugin_pool.register_plugin(MultiChoiceQuestionPlugin)
plugin_pool.register_plugin(DropDownQuestionPlugin)
plugin_pool.register_plugin(GroupedDropDownQuestionPlugin)
plugin_pool.register_plugin(FreeTextQuestionPlugin)
plugin_pool.register_plugin(FreeNumberQuestionPlugin)
plugin_pool.register_plugin(FormNavPlugin)
plugin_pool.register_plugin(SubmissionSetReviewPlugin)
plugin_pool.register_plugin(SectionedScoringPlugin)
plugin_pool.register_plugin(ProgressBarPlugin)
plugin_pool.register_plugin(BulkAnswerPlugin)
plugin_pool.register_plugin(SessionDefinition)
plugin_pool.register_plugin(QuestionnaireTextPlugin)
plugin_pool.register_plugin(TranslatedTextPlugin)
From cms_app.py:
from cms.app_base import CMSApp
from cms.apphook_pool import apphook_pool
from django.utils.translation import ugettext_lazy as _
class CMSSaq(CMSApp):
name = _("Self Assessment")
urls = ["cms_saq.urls"]
apphook_pool.register(CMSSaq)
Additional information:
This duplicative observation creates an issue when trying get the question object via its slug Question.objects.get(slug=question_slug). Such a query should only return one question. What we get here is two questions returned.
import re
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST, require_GET
from django.views.decorators.cache import never_cache
from django.utils import simplejson, datastructures
from django.conf import settings
from cms_saq.models import Question, Answer, Submission, SubmissionSet
ANSWER_RE = re.compile(r'^[\w-]+(,[\w-]+)*$')
#require_POST
def _submit(request):
post_data = datastructures.MultiValueDict(request.POST)
submission_set_tag = post_data.pop('submission_set_tag', '')
for question_slug, answers in post_data.iteritems():
# validate the question
try:
question = Question.objects.get(
slug=question_slug,
#placeholder__page__publisher_is_draft=False,
)
except Question.DoesNotExist:
return HttpResponseBadRequest(
"Invalid question '%s'" % question_slug,
)
# check answers is a list of slugs
if question.question_type != 'F' and not ANSWER_RE.match(answers):
return HttpResponseBadRequest("Invalid answers: %s" % answers)
# validate and score the answer
try:
score = question.score(answers)
except Answer.DoesNotExist:
return HttpResponseBadRequest(
"Invalid answer '%s:%s'" % (question_slug, answers)
)
# save, but don't update submissions belonging to an existing set
filter_attrs = {
'user': request.user.id,
'question': question_slug,
'submission_set': None,
}
attrs = {'answer': answers, 'score': score}
rows = Submission.objects.filter(**filter_attrs).update(**attrs)
if not rows:
attrs.update(filter_attrs)
Submission.objects.create(**attrs)
# Create submission set if requested
if submission_set_tag:
submission_set_tag = submission_set_tag[0]
if submission_set_tag:
_create_submission_set(
request, submission_set_tag
)
return HttpResponse("OK")
If you create a page in CMS, and add plugins to it, once you hit publish the CMS creates a copy of each of those plugins as they are at that point in time. This gives the page a live version, and enables you to then make changes to it which are held in draft mode, until you hit publish again.
This allows you to have a draft version of the site where edits/changes can be made and finalised before making them public.
Seeing two copies for each plugin therefore, isn't a problem.
As a side note, I'd strongly recommend you join the CMS user group on Google Plus if you're developing CMS sites; https://plus.google.com/communities/107689498573071376044
update
Ok, so a plugin in CMS is attached to a placeholder which is on a page, and it's the page that has two version of itself. Because each plugin attaches to a page, you can use that relationship to filter your plugins.
If you register your plugin with admin, or to object calls on it you're just looking at what is stored in your table, which as I say, includes the draft and live version of plugins.
So when you're querying for your plugin do this;
questions = Question.objects.filter(placeholder__page__publisher_is_draft=True)
This will get you all the questions which are attached to draft pages.
The only drawback to this is that plugins are guaranteed to be attached to a Page() object unless the plugin is set to page_only = True but I've only ever been interested in plugins attached to pages so it works for me. See the docs for more info on this.
Furthermore, if you're ever adding CMS page links to any of your plugins, please don't forget to add a similar argument to your model field to ensure that you limit the page objects to draft only;
page_link = models.ForeignKey(
Page,
limit_choices_to={'publisher_is_draft': True},
help_text=_("Link to another page on the site."),
on_delete=models.SET_NULL,
related_name='myapp_page_link'
)
say I have a model such:
class ComponentLength(models.Model):
component_name = models.CharField(max_length=155)
length1 = models.IntegerField()
length2 = models.IntegerField()
length3 = models.IntegerField()
length4 = models.IntegerField()
Now I have a form for the user to select a component, and on the next page I want to display 4 checkboxes for the various length options, which are different for different components.
What is the best way in Django to generate the form with these checkboxes based on the component name (accessible in session data) already selected by the user.
Any help much appreciated.
You could use a normal django form and change its fields upon instantiation, in the init method. Something like this:
class SecondForm(forms.Form):
def __init__(self, *args, **kwargs):
super(SecondForm, self).__init__(*args, **kwargs)
try:
#get the object, something like this:
obj_ = ComponentLength.objects.get(component_name = session.get('component_name_or_whatever_you_stored'))
except:
#handle the error case, e.g:
return
self.fields['length1'] = forms.CheckboxInput(attrs={'value' : obj_.length1 })
self.fields['length2'] = forms.CheckboxInput(attrs={'value' : obj_.length2 })
self.fields['length3'] = forms.CheckboxInput(attrs={'value' : obj_.length3 })
self.fields['length4'] = forms.CheckboxInput(attrs={'value' : obj_.length4 })
#Consider using a hidden input instead of polluting the session variables
#with form data
self.fields['component_length'] = forms.HiddenInput(attrs={'value' : obj_.pk})
The above code is not tested, but I expect it should work. Please let me know how it goes.
Form wizards is exactly what you need.
https://docs.djangoproject.com/en/1.7/ref/contrib/formtools/form-wizard/
See the example shown here https://docs.djangoproject.com/en/1.7/ref/contrib/formtools/form-wizard/#usage-of-namedurlwizardview
And how forms are defined here https://docs.djangoproject.com/en/1.7/ref/contrib/formtools/form-wizard/#conditionally-view-skip-specific-steps
I haven't tested the code below, but it should be something similar to the following:
myapp/forms.py
from django.forms import ModelForm
from myapp.models import ComponentLength
class ComponentLengthNameForm(ModelForm):
class Meta:
model = ComponentLength
fields = ['component_name',]
class ComponentLengthChoicesForm(ModelForm):
class Meta:
model = ComponentLength
fields = ['length1', 'length2', 'length3', 'length4',]
myapp/views.py
from django.contrib.formtools.wizard.views import SessionWizardView
from django.shortcuts import render_to_response
class ComponentWizard(SessionWizardView):
def done(self, form_list, **kwargs):
return render_to_response('done.html', {
'form_data': [form.cleaned_data for form in form_list],
})
myapp/urls.py
from django.conf.urls import url, patterns
from myapp.forms import ComponentLengthNameForm, ComponentLengthChoicesForm
from myapp.views import ContactWizard
named_contact_forms = (
('name', ComponentLengthNameForm),
('length-choices', ComponentLengthChoicesForm),
)
component_wizard = ComponentWizard.as_view(named_contact_forms,
url_name='component-wizard-form-step', done_step_name='finished')
urlpatterns = patterns('',
url(r'^my-form/(?P<step>.+)/$', component_wizard, name='component-wizard-form-step'),
url(r'^my-form/$', component_wizard, name='component-wizard-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})
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.