I have rigged up a custom "Advanced Search" view by slightly extending the flask admin index view. This takes the user to a custom form rendered using the flask admin form rendering machinery to do most of the work.
The form is defined as follows:
class PaperSearchForm(FlaskForm):
return_url = HiddenField()
title = StringField()
abstract = StringField()
start_date = DateTimeField()
end_date = DateTimeField()
doi = StringField()
pubmed_id = StringField()
link = StringField()
journals = QuerySelectMultipleField(
query_factory=_get_model(Journal),
)
authors = QuerySelectMultipleField(
query_factory=_get_model(Author),
)
keywords = QuerySelectMultipleField(
query_factory=_get_model(Keyword),
)
chapters = QuerySelectMultipleField(
query_factory=_get_model(Chapter),
)
printed = BooleanField(default=True)
unprinted = BooleanField(default=True)
submit = SubmitField('Search')
The Advanced Search model view is defined like this:
from flask import flash
from flask import redirect
from flask import request
from flask_admin import BaseView
from flask_admin import expose
from flask_wtf import FlaskForm
from flask_login import current_user
from .forms import PaperSearchForm
class AdvancedPaperSearchView(BaseView):
form_base_class = FlaskForm
def __init__(self,
name=None,
category=None,
endpoint=None,
url=None,
template='auth/model/paper/advanced_search.html',
menu_class_name=None,
menu_icon_type=None,
menu_icon_value=None
):
super(AdvancedPaperSearchView, self).__init__(
name,
category,
endpoint,
url or '/',
'static',
menu_class_name=menu_class_name,
menu_icon_type=menu_icon_type,
menu_icon_value=menu_icon_value)
self._template = template
def is_visible(self):
return False
def is_accessible(self):
if current_user.is_authenticated:
return current_user.can_view_papers()
return False
@expose('/', methods=['GET', 'POST'])
def index(self):
form = PaperSearchForm()
form.return_url.data = request.args['return_url']
self._template_args['form'] = form
self._template_args['cancel_url'] = request.args['return_url']
return self.render(self._template)
@expose('/search', methods=['POST'])
def search(self):
# List view generates list of models based on 'term'= from request.args.get('term', default=None)
# Manually setting these arguments will serve as the advanced search functionality
form = PaperSearchForm() # ???
search = None # ???
filter = None # ???
flash('How to apply multiple filters?', 'error')
return redirect('papermodelview.index', search=search, filter=filter) # ???
Then, the template is defined like this:
{% extends "admin/master.html" %}
{% import 'admin/lib.html' as lib with context %}
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
{% from 'admin/lib.html' import render_field with context %}
{% block head %}
{{ super() }}
{{ lib.form_css() }}
{% endblock %}
{% block body %}
{% block navlinks %}
<ul class="nav nav-tabs">
<li>
<a href="{{ return_url }}">List</a>
</li>
<li class="active">
<a href="javascript:void(0)">Advanced Search</a>
</li>
</ul>
{% endblock %}
<form method="post" action="{{ url_for('advancedpapersearchview.search') }}">
{{ form.return_url }}
{{ form.csrf_token }}
{{ render_field(form, form.title) }}
{{ render_field(form, form.abstract) }}
{{ render_field(form, form.start_date) }}
{{ render_field(form, form.end_date) }}
{{ render_field(form, form.doi) }}
{{ render_field(form, form.pubmed_id) }}
{{ render_field(form, form.link) }}
{{ render_field(form, form.journals) }}
{{ render_field(form, form.authors) }}
{{ render_field(form, form.chapters) }}
{{ render_field(form, form.keywords) }}
{{ render_field(form, form.printed) }}
{{ render_field(form, form.unprinted) }}
<div class="row">
<div class="col-xs-12">
{{ form.submit(class="btn") }}
</div>
</div>
<div class="row">
<div class="col-xs-12">
<a href="{{ cancel_url }}" class="btn warning">
Cancel
</a>
</div>
</div>
</form>
{% endblock %}
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
<script src="/static/vendor/jquery.min.js" type="text/javascript">/script>
{# use /static/bootstrap2/js/bootstrap.min.js if you are using bootstrap2 #}
<script src="/static/bootstrap3/js/bootstrap.min.js" type="text/javascript"></script>
<script src="/static/vendor/moment.min.js" type="text/javascript"></script>
<script src="/static/vendor/select2/select2.min.js" type="text/javascript"></script>
{% endblock %}
In the Paper Model View, the filters are defined like this:
class PaperModelView(MainModelView):
# ...
column_filters = [
'chapter_paper_assoc.printed',
'journal_paper_assoc.publication_date',
'chapters.name',
'chapters.number',
'journals.name',
'authors.last_name',
'keywords.keyword',
]
So, I commented a bunch of ??? where I don't know what to do. How do I map the fields of my form (specific attributes of the models selected) to the filters that are defined by 'column_filters' in the model view.
I.e -- rather than overriding the index view search handling to actually perform a search, I could instead apply a bunch of filters by passing this information to the index view, which retrieves this information with:
filters=response.args.get('filter', None)
Is there a better approach?
Thanks
Alright, things are about to get ugly, so hold onto your hat.
This is what I ended up writing to accomplish this functionality, and it is code that I am not particularly proud to have written. It works, but that said, please suggest a cleaner way to do this if you feel like it.
Here is the advanced search view:
from flask import flash
from flask import redirect
from flask import request
from flask import url_for
from flask_admin import BaseView
from flask_admin import expose
from flask_wtf import FlaskForm
from flask_login import current_user
from app import admin
from .forms import PaperSearchForm
class AdvancedPaperSearchView(BaseView):
form_base_class = FlaskForm
def __init__(self,
name=None,
category=None,
endpoint=None,
url=None,
template='auth/model/paper/advanced_search.html',
menu_class_name=None,
menu_icon_type=None,
menu_icon_value=None
):
super(AdvancedPaperSearchView, self).__init__(
name,
category,
endpoint,
url or '/',
'static',
menu_class_name=menu_class_name,
menu_icon_type=menu_icon_type,
menu_icon_value=menu_icon_value)
self._template = template
def is_visible(self):
return False
def is_accessible(self):
if current_user.is_authenticated:
return current_user.can_view_papers()
return False
@expose('/', methods=['GET', 'POST'])
def index(self):
form = PaperSearchForm()
form.return_url.data = request.args['return_url']
self._template_args['form'] = form
self._template_args['cancel_url'] = request.args['return_url']
return self.render(self._template)
@expose('/search', methods=['POST'])
def search(self):
form = PaperSearchForm()
# The goal here is to get the paper model view from the currently running app (and its admin extension). Once
# the model view is here, use it to get the available filters (get their keys and operations). Use the existing
# request args and add filters to them using the key and operations defined in the model view.
paper_model_view = None
for view in admin._views:
# There must be a better way to do this, and I know this is a WTF, but I don't have the vocabulary to search
# the flask admin documentation for the right way to get the instance of the model view from the admin
# object. I need the *instance*, with the filters created and added to that instance by the init... so...
# not clean or pretty ... and permanently restricts name of paper model view ... TODO: Fix? Rewrite?
# - Chris, March 2017
if "PaperModelView object" in view.__repr__():
paper_model_view = view
# ._filters contains the list of all filters
# ._filter_args contains a dictionary of keys and filter objects for url construction
# each filter is persisted with request.args, the query string is <flt[position]_[key]=[value]> or
# <flt[position]_[key]> for filters without values
# The filter is accessed by looking up the filter object with the key value, and then the filters are listed
# in the order of the position provided in the query string. I am unsure whether or not they are applied in
# this order, but that seems like what is happening.
filters = {}
i = 0
str = "flt{pos}_{key}"
def __check(column, table, filter):
return (column in filter.column.name and table in filter.column.table.name.__repr__())
# Sorry for this...
# Iterate through each filter available for the view. Check if it's name and operation are something that
# will enact a portion of the search, then add it's filter (in the format expected) to a dictionary. The index
# variable i keeps track of the "count" of filters that have been added and uses this as the position of the
# filter.
for key, key_filter in paper_model_view._filter_args.items():
filter = key_filter[1]
if hasattr(filter, 'column'):
if __check("title", "papers", filter):
if "FilterLike" in filter.operation.__repr__():
if form.title.data:
filters[str.format(pos=i, key=key)] = form.title.data
i += 1
if __check("abstract", "papers", filter):
if "FilterLike" in filter.operation.__repr__():
if form.abstract.data:
filters[str.format(pos=i, key=key)] = form.abstract.data
i += 1
if __check("publication_date", "journal_paper", filter):
if "DateSmaller" in filter.operation.__repr__():
if form.end_date.data:
filters[str.format(pos=i, key=key)] = form.end_date.data.date() # Only keeps the date for the filter
i += 1
elif "DateGreater" in filter.operation.__repr__():
if form.start_date.data:
filters[str.format(pos=i, key=key)] = form.start_date.data.date()
i += 1
if __check("doi", "papers", filter):
if "FilterLike" in filter.operation.__repr__():
if form.doi.data:
filters[str.format(pos=i, key=key)] = form.doi.data
i += 1
if __check("pubmed_id", "papers", filter):
if "FilterLike" in filter.operation.__repr__():
if form.pubmed_id.data:
filters[str.format(pos=i, key=key)] = form.pubmed_id.data
i += 1
if __check("link", "papers", filter):
if "FilterLike" in filter.operation.__repr__():
if form.link.data:
filters[str.format(pos=i, key=key)] = form.link.data
i += 1
if __check("name", "journal", filter):
if "FilterLike" in filter.operation.__repr__():
if form.journals.data:
for journal in form.journals.data:
filters[str.format(pos=i, key=key)] = journal.name
i += 1
if __check("first_name", "authors", filter):
if "FilterLike" in filter.operation.__repr__():
for author in form.authors.data:
filters[str.format(pos=i, key=key)] = author.first_name
i += 1
if __check("last_name", "authors", filter):
if "FilterLike" in filter.operation.__repr__():
for author in form.authors.data:
filters[str.format(pos=i, key=key)] = author.last_name
i += 1
if __check("keyword", "keywords", filter):
if "FilterLike" in filter.operation.__repr__():
for keyword in form.keywords.data:
filters[str.format(pos=i, key=key)] = keyword.keyword
i += 1
if __check("name", "chapters", filter):
if "FilterLike" in filter.operation.__repr__():
for chapter in form.chapters.data:
filters[str.format(pos=i, key=key)] = chapter.name
i += 1
if __check("printed", "chapter_paper", filter):
if "FilterEqual" in filter.operation.__repr__():
if form.printed.data == True:
if form.unprinted.data == False: # Printed only
filters[str.format(pos=i, key=key)] = 1 # True
i += 1
else:
pass # Both are True
else:
if form.unprinted.data == True: # Unprinted only
filters[str.format(pos=i, key=key)] = 0 # False
i += 1
else:
pass # Both are False
else:
continue
flash('Filters successfully applied', 'success')
return redirect(url_for('paper.index_view', **filters))