django - simplest way to customize admin search - python

I'm using django 1.8.
What I need is to do case insensitive admin-search in multiple fields and allow the user to use the AND, OR and NOT operators and some how group words either with parentheses or quotes.
Search Example:
cotton and (red or "dark blue")
I've already discovered django-advanced-filter and django-filter...
They are filters! I also want to allow the user to type in the keys in the search box.
I know that get_search_results allows us to override the search behaviour, but before I write a code for this, I want to ask is there a package that would do this for me?
Note that I feel that making a custom search with haystack is pretty complex.

This answer seems to work for me after performing the little edit mentioned in my comment. Yet, I have no idea whether this is the "correct" way of doing it.
Here is the updated code that works on django 1.8:
from django.contrib import admin
from django.db import models
from bookstore.models import Book
from django.contrib.admin.views.main import ChangeList
import operator
class MyChangeList(ChangeList):
def __init__(self, *a):
super(MyChangeList, self).__init__(*a)
def get_queryset(self, request):
print dir(self)
# First, we collect all the declared list filters.
(self.filter_specs, self.has_filters, remaining_lookup_params,
use_distinct) = self.get_filters(request)
# Then, we let every list filter modify the queryset to its liking.
qs = self.root_queryset
for filter_spec in self.filter_specs:
new_qs = filter_spec.queryset(request, qs)
if new_qs is not None:
qs = new_qs
try:
# Finally, we apply the remaining lookup parameters from the query
# string (i.e. those that haven't already been processed by the
# filters).
qs = qs.filter(**remaining_lookup_params)
except (SuspiciousOperation, ImproperlyConfigured):
# Allow certain types of errors to be re-raised as-is so that the
# caller can treat them in a special way.
raise
except Exception, e:
# Every other error is caught with a naked except, because we don't
# have any other way of validating lookup parameters. They might be
# invalid if the keyword arguments are incorrect, or if the values
# are not in the correct type, so we might get FieldError,
# ValueError, ValidationError, or ?.
raise IncorrectLookupParameters(e)
# Use select_related() if one of the list_display options is a field
# with a relationship and the provided queryset doesn't already have
# select_related defined.
if not qs.query.select_related:
if self.list_select_related:
qs = qs.select_related()
else:
for field_name in self.list_display:
try:
field = self.lookup_opts.get_field(field_name)
except Exception as ex:# models.FieldDoesNotExist:
print ex
pass
else:
if isinstance(field.rel, models.ManyToOneRel):
qs = qs.select_related()
break
# Set ordering.
ordering = self.get_ordering(request, qs)
qs = qs.order_by(*ordering)
# Apply keyword searches.
def construct_search(field_name):
if field_name.startswith('^'):
return "%s__istartswith" % field_name[1:]
elif field_name.startswith('='):
return "%s__iexact" % field_name[1:]
elif field_name.startswith('#'):
return "%s__search" % field_name[1:]
else:
return "%s__icontains" % field_name
if self.search_fields and self.query:
orm_lookups = [construct_search(str(search_field))
for search_field in self.search_fields]
or_queries = []
for bit in self.query.split():
or_queries += [models.Q(**{orm_lookup: bit})
for orm_lookup in orm_lookups]
if len(or_queries) > 0:
qs = qs.filter(reduce(operator.or_, or_queries))
if not use_distinct:
for search_spec in orm_lookups:
if admin.utils.lookup_needs_distinct(self.lookup_opts, search_spec):
use_distinct = True
break
if use_distinct:
return qs.distinct()
else:
return qs
#admin.register(Book)
class AdminBookstore(admin.ModelAdmin):
list_display = ('title', 'author', 'description')
search_fields = ('title', 'author', 'description')
def get_changelist(*a, **k):
return MyChangeList

Related

Wagtail/Django: Query filter to return only the pages the user has acess/permissions?

I'm going through the documentation at: http://docs.wagtail.io/en/v2.7.1/reference/pages/queryset_reference.html.
Is there a filter to return only the pages the user has access to? I can only see public() and not_public().
I have some pages which privacy is set to Private (accessible to users in specific groups). And would like to exclude them from the query results.
There is no such filter in PageQuerySet. You can however create your own QuerySet that adds an authorized filter and use that. The following code comes from the Joyous events EventQuerySet and is based upon PageQuerySet.public_q and BaseViewRestriction.accept_request. It gets all the restrictions that could apply, excludes the ones that the user passes, and then filters out the pages with the remaining restrictions.
from wagtail.core.query import PageQuerySet
from wagtail.core.models import Page, PageManager, PageViewRestriction
class MyQuerySet(PageQuerySet):
def authorized_q(self, request):
PASSWORD = PageViewRestriction.PASSWORD
LOGIN = PageViewRestriction.LOGIN
GROUPS = PageViewRestriction.GROUPS
KEY = PageViewRestriction.passed_view_restrictions_session_key
restrictions = PageViewRestriction.objects.all()
passed = request.session.get(KEY, [])
if passed:
restrictions = restrictions.exclude(id__in=passed,
restriction_type=PASSWORD)
if request.user.is_authenticated:
restrictions = restrictions.exclude(restriction_type=LOGIN)
if request.user.is_superuser:
restrictions = restrictions.exclude(restriction_type=GROUPS)
else:
membership = request.user.groups.all()
if membership:
restrictions = restrictions.exclude(groups__in=membership,
restriction_type=GROUPS)
q = models.Q()
for restriction in restrictions:
q &= ~self.descendant_of_q(restriction.page, inclusive=True)
return q
def authorized(self, request):
self.request = request
if request is None:
return self
else:
return self.filter(self.authorized_q(request))
You could then set this to be your model's default QuerySet.
class MyPage(Page):
objects = PageManager.from_queryset(MyQuerySet)()
Then when filtering your MyPage objects you can say MyPage.objects.live().authorized(request).all()
Hope that is helpful. May contain bugs.

Terrible Django Admin Performance

I am developing a Django project, which already got plenty of real world data so I can see its performance.
The performances of few DjangoAdmin lists are just terrible.
I have one admin list, lets call it devices. In that list I am fetching additional informations for each row, those fields are related from other tables and connected via FK/PK/M2N.
List contains about 500 records and loading of that screen takes, according to django-debug-toolbar, around 6.5 seconds, which is unbearable.
This admin class
#admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
list_select_related = True
list_display = ('id', 'name', 'project', 'location', 'machine', 'type', 'last_maintenance_log')
inlines = [CommentInline, TestLogInline]
def project(self, obj):
try:
return Device.objects.get(pk=obj.pk).machine.location.project.project_name
except AttributeError:
return '-'
def location(self, obj):
try:
return Device.objects.get(pk=obj.pk).machine.location.name
except AttributeError:
return '-'
def last_maintenance_log(self, obj):
try:
log = AdminLog.objects.filter(object_id=obj.pk).latest('time')
return '{} | {}'.format(log.time.strftime("%d/%m/%Y, %-I:%M %p"), log.title)
except AttributeError:
return '-'
All Machine, Location and Project are tables in database.
After looking into queries in django-debug-toolbar I discovered something terrible.
That screen required 287 sql queries ! Yes, over two hundreds!
Is there anything I can do to reduce this time to something reasonable?
EDIT:
Thanks to Bruno I removed Device.objects.get(pk=obj.id) (as it was really redundant, I totally overlooked this.
So everywhere i put obj. for example obj.machine.location.project.project_name
Only this alone reduces the speed and query count to half, so far so good.
I not have trouble fusing obj approach and select_related approach. This is my current code, which is worse than only obj approach.
def project(self, obj):
try:
Device.objects.select_related('machine__location__project').get(id=obj.pk).machine.location.project.project_name
except AttributeError:
return '-'
Which creates a nice INNER JOINs in queries, but the performance is around 15% worse than without it and using only obj.machine.location.project.project_name
What am I doing wrong?
EDIT2:
Best performance I got is with this code:
#admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
list_select_related = True
save_as = True
form = DeviceForm
list_display = ('id', 'name', 'project', 'location', 'machine', 'type', 'last_maintenance_log')
inlines = [CommentInline, TestLogInline]
def project(self, obj):
try:
return obj.machine.location.project.project_name
except AttributeError:
return '-'
def location(self, obj):
try:
return obj.machine.location.name
except AttributeError:
return '-'
def last_maintenance_log(self, obj):
try:
log = AdminLog.objects.filter(object_id=obj.pk).latest('time')
return '{} | {}'.format(log.time.strftime("%d/%m/%Y, %-I:%M %p"), log.title)
except AttributeError:
return '-'
def get_queryset(self, request):
return Device.objects.select_related('machine__location__project').all()
Which pushed this down to 104 queries (from almost 300) and time reduction over 50%. Can this be improved any further?
First, avoid totally useless queries - this:
Device.objects.get(pk=obj.pk)
is as useless as can be since obj already the Device instance you are looking for.
Then override your DeviceAdmin.get_queryset method to make proper use of select_related and prefetch_related so you only have the minimal required number of queries.

WTForms create variable number of fields

How I would dynamically create a few form fields with different questions, but the same answers?
from wtforms import Form, RadioField
from wtforms.validators import Required
class VariableForm(Form):
def __init__(formdata=None, obj=None, prefix='', **kwargs):
super(VariableForm, self).__init__(formdata, obj, prefix, **kwargs)
questions = kwargs['questions']
// How to to dynamically create three questions formatted as below?
question = RadioField(
# question ?,
[Required()],
choices = [('yes', 'Yes'), ('no', 'No')],
)
questions = ("Do you like peas?", "Do you like tea?", "Are you nice?")
form = VariableForm(questions = questions)
It was in the docs all along.
def my_view():
class F(MyBaseForm):
pass
F.username = TextField('username')
for name in iterate_some_model_dynamically():
setattr(F, name, TextField(name.title()))
form = F(request.POST, ...)
# do view stuff
What I didn't realize is that the class attributes must be set before any instantiation occurs. The clarity comes from this bitbucket comment:
This is not a bug, it is by design. There are a lot of problems with
adding fields to instantiated forms - For example, data comes in
through the Form constructor.
If you reread the thread you link, you'll notice you need to derive the class, add fields to that, and then instantiate the new class. Typically you'll do this inside your view handler.
You're almost there:
CHOICES = [('yes', 'Yes'), ('no', 'No')]
class VariableForm(Form):
def __new__(cls, questions, **kwargs):
for index, question in enumerate(questions):
field_name = "question_{}".format(index)
field = RadioField(question,
validators=[Required()],
choices=CHOICES)
setattr(cls, field_name, field)
return super(VariableForm, cls).__new__(cls, **kwargs)
In my case,
I used a csv and imported it using pandas.
So, this solution allows you to even use different answers if required.
data=pd.read_csv("./temp.csv")
class UserForm(Form):
for i in data:
if data[i][0] == 'textbox':
formElement='TextField("%s",validators=[validators.required()], default="please add content")' %(i)
elif data[i][0] == 'radio':
choice = list(data[i][1:].dropna().unique().tolist())
choiceStr=''
for k in choice:
choiceStr +="('"+k+"','"+k+"'),"
formElement = 'RadioField("%s",validators=[validators.required()],choices=[%s], default="%s")' %(i,choiceStr, choice[0])
elif data[i][0] == 'dropdown':
choice = list(data[i][1:].dropna().unique().tolist())
# choice.remove('X')
choiceStr=''
for k in choice:
choiceStr +="('"+k+"','"+k+"'),"
formElement = 'SelectField("%s",validators=[validators.required()],choices=[%s])' %(i,choiceStr)
exec("%s=%s" % (i,formElement))

Combine several filters into one filter() with django-filters

I'm using django-filter app. There is however one problem I do not know how to solve. It's almost exactly the same thing as is described in django documentation:
https://docs.djangoproject.com/en/1.2/topics/db/queries/#spanning-multi-valued-relationships
I want to make a query where I select all Blogs that has an entry with both "Lennon" in headline and was published in 2008, eg.:
Blog.objects.filter(entry__headline__contains='Lennon',
entry__pub_date__year=2008)
Not to select Blogs that has an entry with "Lennon" in headline and another entry (possibly the same) that was published in 2008:
Blog.objects.filter(entry__headline__contains='Lennon').filter(
entry__pub_date__year=2008)
However, if I set up Filter such that there are two fields (nevermind __contains x __exact, just an example):
class BlogFilter(django_filters.FilterSet):
entry__headline = django_filters.CharFilter()
entry__pub_date = django_filters.CharFilter()
class Meta:
model = Blog
fields = ['entry__headline', 'entry__pub_date', ]
django-filter will generete the latter:
Blog.objects.filter(entry__headline__exact='Lennon').filter(
entry__pub_date__exact=2008)
Is there a way to combine both filters into a single filter field?
Well, I came with a solution. It is not possible to do using the regular django-filters, so I extended it a bit. Could've been improved, this is quick-n-dirty solution.
1st added a custom "grouped" field to django_filters.Filter and a filter_grouped method (almost copy of filter method)
class Filter(object):
def __init__(self, name=None, label=None, widget=None, action=None,
lookup_type='exact', required=False, grouped=False, **kwargs):
(...)
self.grouped = grouped
def filter_grouped(self, qs, value):
if isinstance(value, (list, tuple)):
lookup = str(value[1])
if not lookup:
lookup = 'exact' # we fallback to exact if no choice for lookup is provided
value = value[0]
else:
lookup = self.lookup_type
if value:
return {'%s__%s' % (self.name, lookup): value}
return {}
the only difference is that instead of creating a filter on query set, it returns a dictionary.
2nd updated BaseFilterSet qs method/property:
class BaseFilterSet(object):
(...)
#property
def qs(self):
if not hasattr(self, '_qs'):
qs = self.queryset.all()
grouped_dict = {}
for name, filter_ in self.filters.iteritems():
try:
if self.is_bound:
data = self.form[name].data
else:
data = self.form.initial.get(name, self.form[name].field.initial)
val = self.form.fields[name].clean(data)
if filter_.grouped:
grouped_dict.update(filter_.filter_grouped(qs, val))
else:
qs = filter_.filter(qs, val)
except forms.ValidationError:
pass
if grouped_dict:
qs = qs.filter(**grouped_dict)
(...)
return self._qs
The trick is to store all "grouped" filters in a dictionary and then use them all as a single filter.
The filter will look something like this then:
class BlogFilter(django_filters.FilterSet):
entry__headline = django_filters.CharFilter(grouped=True)
entry__pub_date = django_filters.CharFilter(grouped=True)
class Meta:
model = Blog
fields = ['entry__headline', 'entry__pub_date', ]

Skip steps on a django FormWizard

I have an application where there is a FormWizard with 5 steps, one of them should only appear when some conditions are satisfied.
The form is for a payment wizard on a on-line cart, one of the steps should only show when there are promotions available for piking one, but when there are no promotions i want to skip that step instead of showing an empty list of promotions.
So I want to have 2 possible flows:
step1 - step2 - step3
step1 - step3
The hook method process_step() gives you exactly that opportunity.
After the form is validated you can modify the self.form_list variable, and delete the forms you don't need.
Needles to say if you logic is very complicated, you are better served creating separate views for each step/form, and forgoing the FormWizard altogether.
To make certain forms optional you can introduce conditionals in the list of forms you pass to the FormView in your urls.py:
contact_forms = [ContactForm1, ContactForm2]
urlpatterns = patterns('',
(r'^contact/$', ContactWizard.as_view(contact_forms,
condition_dict={'1': show_message_form_condition}
)),
)
For a full example see the Django docs: https://django-formtools.readthedocs.io/en/latest/wizard.html#conditionally-view-skip-specific-steps
I did it other way, overriding the render_template method. Here my solution. I didn't know about the process_step()...
def render_template(self, request, form, previous_fields, step, context):
if not step == 0:
# A workarround to find the type value!
attr = 'name="0-type" value='
attr_pos = previous_fields.find(attr) + len(attr)
val = previous_fields[attr_pos:attr_pos+4]
type = int(val.split('"')[1])
if step == 2 and (not type == 1 and not type == 2 and not type == 3):
form = self.get_form(step+1)
return super(ProductWizard, self).render_template(request, form, previous_fields, step+1, context)
return super(ProductWizard, self).render_template(request, form, previous_fields, step, context)
There are different ways for this(as mentioned in other answers), but one solution which I think could be useful is overwriting the get_form_list() method:
something like:
from collections import OrderedDict
def get_form_list(self):
form_list = OrderedDict()
// add some condition based on the earlier forms
cleaned_data = self.get_cleaned_data_for_step('step1') or {}
for form_key, form_class in self.form_list.items():
if cleaned_data and cleaned_data['step1'] == 'X':
if form_key == 'step2':
#skip step2
continue
else:
pass
elif cleaned_data and cleaned_data['step1'] == 'Y':
if form_key == 'step4':
#skip step4
continue
else:
pass
....
# try to fetch the value from condition list, by default, the form
# gets passed to the new list.
condition = self.condition_dict.get(form_key, True)
if callable(condition):
# call the value if needed, passes the current instance.
condition = condition(self)
if condition:
form_list[form_key] = form_class
return form_list
I think in this way you can handle complicated forms and you'r not gonna have any conflict with other stuffs.

Categories