dynamically chaining q objects with &/|

26 views
Skip to first unread message

Sam Raker

unread,
Jan 2, 2013, 1:41:48 AM1/2/13
to django...@googlegroups.com
I want users to be able to query the database via different rows (name, year, etc.), with the option to have multiple fields connected with boolean operators, e.g. "year 1900 AND name chicken OR location San Francisco." I'm using Q objects and reduce(), but I've clearly made some mistake, since when I try just one field with a value I know should return stuff (e.g. through making the same query via the django shell), I get nothing. Anyone have any idea what I'm doing wrong/how I could fix it?

Here's my view:

def search(request):
errors = []
searchFormSet = formset_factory(F.SearchForm, extra=1,formset=BaseSearchFormSet)
if request.GET:
formset = searchFormSet(request.GET)
forms = []
if formset.is_valid():
for x in xrange(0,formset.total_form_count()):
form = {}
form["row"]= formset[x].cleaned_data.get("row",None)
form["query"] = formset[x].cleaned_data.get("query",None)
form["bools"] = formset[x].cleaned_data.get("bools",None)
if form["query"]:
q = form["query"]
else:
errors.append("no query found")
if form["row"]:
row = form["row"]
else:
errors.append("no row found")
filter_keys = {"dish_name":Q(dish__name__icontains=q),
"regex":Q(dish__full_name__regex=r'%s'%(q)),
"course":Q(dish__classification=q.lower()),
"year":Q(page__menu_id__year__exact=q),
"period":Q(page__menu_id__period__exact=q),
"location":Q(page__menu_id__location__icontains=q),
"restaurant":Q(page__menu_id__restaurant__icontains=q)}
forms.append(form)
final_query=Q()
def var_reduce(op,slice):
if op == "and":
return reduce(lambda x,y: x & y,slice)
elif op == "or":
return reduce(lambda x,y: x | y,slice)
for x in xrange(len(forms)):
try:
try:
if final_query:
slice = [final_query,filter_keys[forms[x]["row"]],filter_keys[forms[x+1]["row"]]]
else:
slice = [filter_keys[forms[x]["row"]],filter_keys[forms[x+1]["row"]]]
final_query = var_reduce(forms[x]["bools"],slice)
except IndexError:
if final_query:
slice = [final_query,filter_keys[forms[x]["row"]]]
else:
slice = [filter_keys[forms[x]["row"]]]
final_query = var_reduce(forms[x]["bools"],slice)
items = MenuItem.objects.filter(final_query)
# classifications = {}
# for dish in dishes:
# classifications["{d}".format(dish)] = dish.dish_set.all().classification_set.all()
return render_to_response("search_results.html",{"items":items,"formset":formset})
except KeyError as e:
errors.append(e)
formset = searchFormSet(None)
return render_to_response("search_page.html",{"errors":errors,"formset":formset})
else:
formset = searchFormSet(None)
return render_to_response("search_page.html",{"errors":errors,"formset":formset})
else:
formset = searchFormSet(None)
return render_to_response("search_page.html",{"formset":formset})

Here're my models:

from django.db import models

# Create your models here.
# class MenuItemManager(models.Manager):
# def create_MenuItem(self,dish,price,course,page,mk):
# menuItem = self.create(dish=str(dish),price=str(price),page=str(page),mk=str(mk))
# return menuItem
class MenuItem(models.Model):
def format_price(self):
return "${0:0<4,.2f}".format(float(self.price))
def __unicode__(self):
return self.dish
dish=models.OneToOneField('Dish',to_field='mk')
price=models.CharField(max_length=5,blank=True)
# course=models.CharField(max_length=100,unique=False)
page=models.OneToOneField('MenuPage')
mk=models.CharField(max_length=10,unique=True)
formatted_price = property(format_price)
#objects=MenuItemManager()
# def __unicode__(self):
# return name
# class MenuManager(models.Manager):
# def create_Menu(self,restaurant,year,location,status,mk,period,language):
# menu = self.create(restaurant=str(restaurant),year=int(year),location=str(location),status=str(status),mk=int(mk),period=str(period),language=str(language))
# return menu
class Menu(models.Model):
try:
p=int(10*round(float(int(self.year))/10))
if p < self.year:
return "%s-%s"%(p,p+5)
else:
return "%s-%s"%(p-5,p)
except (ValueError,TypeError):
return ""
def __unicode__(self):
if self.restaurant:
return self.restaurant
else:
return self.mk
restaurant=models.TextField(unique=False,blank=True,null=True)
year=models.CharField(max_length=4,unique=False,blank=True,null=True)
location=models.TextField(unique=False,blank=True,null=True)
status=models.CharField(unique=False,max_length=20)
mk=models.CharField(max_length=8,unique=True,primary_key=True)
period=property(period)
language = models.CharField(unique=False,max_length=30)
#objects=MenuManager()
# def __unicode__(self):
# return restaurant
# class MenuPageManager(models.Manager):
# def create_MenuPage(self,mk,menu_id):
# menuPage = self.create(mk=int(mk),menu_id=int(menu_id))
# return menuPage
class MenuPage(models.Model):
mk=models.CharField(max_length=10,unique=True)
menu_id=models.OneToOneField("Menu",to_field='mk')
#objects=MenuPageManager()
# class DishManager(models.Manager):
# def create_Dish(self,name,mk):
# dish = self.create(name=str(name),mk=int(mk))
# return dish
# def get_by_natural_key(self,name):
# return self.get(name=name)
class Dish(models.Model):
def __unicode__(self):
return self.name
full_name = models.TextField()
name=models.CharField(unique=True,max_length=255)
mk=models.CharField(max_length=10,unique=True)
#classification = models.OneToOneField("Classification",to_field="d")
#objects=DishManager()
# def __unicode__(self):
# return name
# class ClassificationManager(models.Manager):
# def create_Classification(self,dish,classification,mk):
# classification = self.create(dish=str(dish),classification=str(classification),mk=int(mk))
# return classification
class Classification(models.Model):
def __unicode__(self):
if self.classification:
return self.classification
else:
return "none"
dish=models.OneToOneField('dish',to_field='name')
classification=models.CharField(unique=False,max_length=9)
mk=models.CharField(max_length=10,primary_key=True)

Here, if it's relevant, is the template for my search page:

{% extends "base.html" %}
{% block style %}
<link rel="stylesheet" type="text/css" href="/static/search_style.css" />
{% endblock %}
{% block java %}
<script type="text/javascript" src="/static/searches.js"></script>
{% endblock %}
{% block title %}Search{% endblock %}
{% block head %}Search{% endblock %}
{% block content %}
{% autoescape off %}
<div id="searches">
<form id="search" action="" method="get">
<table border="0" cellpadding="0" cellspace="0">
<tbody class="search">
{% for form in formset.forms %}
<tr>
<td class="row">{{ form.row }}</td>
<td class="query">{{ form.query }}</td>
<td class="bool">{{ form.bools }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ formset.management_form }}
<input type="submit" value="Submit" id="submit">
</form>
</div>
{% if formset.errors or errors %}
<div id="errors">
<h3>The following errors were encountered while trying to submit your search:</h3>
{% for x,y in formset.errors.items %}
<p>{{ x }} : {{ y }}</p>
{% endfor %}
{{ errors }}
</div>
{% endif %}
<div id="notes">
<p>Searching by dish names, locations, and restaurants is case-insensitive.</p>
<p>Searching by course uses  case-insensitive exact matching. Valid courses are Appetizer, Main, and Dessert.</p>
<p>Years should be entered YYYY. Five-year periods span either the first or second half of a decade, and should be entered YYYY-YYYY. Example valid five-year periods are 1900-1905, 1995-2000, etc.</p>
<p>Regular expression search follows MySQL regular expression syntax, as described <a href="http://dev.mysql.com/doc/refman/5.1/en/regexp.html" target="_blank">here</a>.</p>
</div>

{% endautoescape %}
<br /><br /><br /><br /> <br /><br /><br />
{% endblock %}

{% block footer %}
<div id="warning">
<p>NOTE: This site and the information it contains are still in development. Some information may be missing or inaccurate.</p>
</div>
<div class="credits">
Created and maintained by <a href="/about#sam">Sam Raker</a> and <a href="/about#rachel">Rachel Rakov</a>
<br />
Data graciously provided by <a href="http://menus.nypl.org" target="_blank">What's on the Menu?</a>
</div>
{% endblock %}

Peter of the Norse

unread,
Jan 5, 2013, 7:24:33 PM1/5/13
to django...@googlegroups.com
I’ve ben going over your code and it looks like everything is wrong. Reduce is unnecessary here. filter_keys is clobbered each time through the formset loop. You're double adding terms to the final_query. How I would do it:

SearchFormSet = formset_factory(F.SearchForm, extra=1,formset=BaseSearchFormSet) 

def search(request):
try:
formset = SearchFormSet(request.GET)
except ValidationError:
formset = SearchFormSet()
if not formset.is_valid():
return render_to_response("search_page.html",{"formset":formset})
filter = Q()
for data in formset.cleaned_data:
row = data['row']
q = form['query']
bools = form['bools']

if row == "dish_name":
filter_term = Q(dish__name__icontains=q),
elif row == "regex":
filter_term = Q(dish__full_name__regex=q),
elif row == "course":
filter_term = Q(dish__classification=q.lower()),
elif row == "year":
filter_term = Q(page__menu_id__year__exact=q),
elif row == "period":
filter_term = Q(page__menu_id__period__exact=q),
elif row == "location":
filter_term = Q(page__menu_id__location__icontains=q),
elif row == "restaurant":
filter_term = Q(page__menu_id__restaurant__icontains=q)}

if bools == 'or':
filter = filter | filter_term
else
filter = filter & filter_term
items = MenuItem.objects.filter(filter)
return render_to_response("search_results.html",{"items":items,"formset":formset})

You can print out the finial Q object to see exactly what it’s doing to verify that it is actually what you want. In fact, I'd try that right away.

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To view this discussion on the web visit https://groups.google.com/d/msg/django-users/-/089Rexbg5jYJ.
To post to this group, send email to django...@googlegroups.com.
To unsubscribe from this group, send email to django-users...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-users?hl=en.

Peter of the Norse



Reply all
Reply to author
Forward
0 new messages