Pass SQLAlchemy table column to Flask route during search - flask

I am rather new to Flask and web development in general and am struggling to figure out how to set up arbitrary column searching.
My form:
class SearchForm(FlaskForm):
choices = [('facilityid', 'facilityid'),
('facilityname', 'facilityname'),
('networkid', 'networkid'),
('contractid', 'contractid'),
('address', 'address')]
select = SelectField('Search for something', choices=choices)
id_search = StringField('')
submit = SubmitField('Search')
My Route:
#app.route('/results')
def results(search):
search_string = search.data['id_search']
search_field = search.data['select']
# TODO: Remove redundant code
if search_string == '':
query = FGymNetworkFacilities.query.order_by(func.rand()).offset(20).limit(10).all()
table = FacilityTable(query)
table.border = True
elif search_field == 'address' and search_field != '':
query = FGymNetworkFacilities.query.filter(FGymNetworkFacilities.address.like(f'%{search_string}%')).all()
table = FacilityTable(query)
table.border = True
if not results:
flash('no results!')
return redirect('/')
return render_template('facilities.html', table=table)
This table model has around 80 columns and I'd like to not make 80 if/else statements. I need to find a way to pass the column name to the line (in this example address)
query = FGymNetworkFacilities.query.filter(FGymNetworkFacilities.address.like(f'%{search_string}%')).all()
But if there is a better way to do this, I'm definitely open to that. Unfortunately Elastic Search is not something I can implement given the constraints of this project and am forced to use this type of search method for now.
Thanks in advance.

Related

Putting together a complex and changeable query to filter a table

I want to enable filtering on a table. Some of the filters are simple, eg a dropdown to select user. But others are more complex, eg the from date needs to display table rows that have a date that's greater than the filter date. And the search box should search multiple fields in the table.
I started attacking the problem by using a lot of IF statements to built a search query based on the filters the user has applied. I then apply the query to model.objects.filter(a_string_built_depending_on_filters). So far so good but it looks like I'm going to have to start using Q() objects. I'm not sure if I can string together Q() queries in the same way.
#login_required
def entries_show_all(request):
journal_entry_filter_form = JournalEntryFilterForm()
line_item_filter_form = LineItemFilterForm()
order_by = request.GET.get('order_by', '-journal_entry')
filter_query = dict()
url_params = ""
if request.GET.get('user') and int(request.GET.get('user')):
filter_query['journal_entry__user'] = str(request.GET.get('user'))
url_params+='user='+urllib.parse.quote_plus(str(request.GET.get('user')))+'&'
if request.GET.get('type'):
filter_query['journal_entry__type'] = request.GET.get('type')
url_params+='type='+urllib.parse.quote_plus(request.GET.get('type'))+'&'
if filter_query:
#logger.warning('Filter query:')
#logger.warning(filter_query)
#logger.warning('URL Params:'+url_params)
line_items = LineItem.objects.filter(**filter_query).order_by(order_by)
else:
logger.warning('Filters have not been set')
line_items = LineItem.objects.all().order_by(order_by)
paginator = Paginator(line_items,20)
page = request.GET.get('page')
line_items2 = paginator.get_page(page)
return render(request,"journal/entries_show_all.html", {'line_items': line_items2, })
The URL Params variable is used in the template to add to the pagination links, so that the filter holds together while the user moves through pages.
Have found django-filter and started implementing this add-on. Seems to do most of what I want it to.

Remove duplicates from QuerySelectField

i'm running into issues with the following, and I'm wondering if it is even possible.
I have a flask-admin adminview setup, with an extra form field which shows a dropdown based on a specific column (category) in the sql model. See code for clarification:
model:
class Item(db.Model):
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(128), index = True)
category = db.Column(db.String(16))
I have the extra form field in Flask-Admin as follows:
form_extra_fields = {
'category': QuerySelectField(
label='Categories',
query_factory = lambda: db.session.query(Item),
get_label = 'category',
)
}
This all works fine except if there are duplicates in the category column, then the dropdown is populated with these duplicate values. Is it possible to remove those duplicates from dropdown, or at least only show the unique values?
Basically I solved this by overriding a class method in QuerySelectField class as follows, by appending unique labels to a list and check if every next label is in that list. I'm still thinking there should be a better way though...
def iter_choices(self):
labels = []
if self.allow_blank:
yield ('__None', self.blank_text, self.data is None)
for pk, obj in self._get_object_list():
if self.get_label(obj) not in labels:
labels.append(self.get_label(obj))
yield (pk, self.get_label(obj), obj == self.data)

django model search form

Firstly, I did my homework and looked around before posting! My question seems like a very basic thing that must’ve been covered before.
I'm now looking at Django-filter as a potential solution, but would like some advice on if this is the right way to go and if there any other solutions.
I have a Django app wit 10 models, each model has a few fields. Most fields are ChoiceField that users populate using forms with the default select widget. There is a separate form for each model.
I want to create a separate form for each model (in separate views) that users will use to search the database. The search form will contain only drop-down boxes (the select widgets) with the same choices as the forms used to populate the database with the addition of the “any” option.
I know how to use .object.filter(), however the “any” option would correspond to not include specific fields in the filter and I'm not sure how to add model fields to the filter based on users’ selection
I briefly looked at Haystack as an option but it seems to be made for full text search rather than “model filed search” I'm after.
Sample model (simplified):
class Property():
TYPE_CHOICES = (‘apartment’, ‘house’, ‘flat’)
type = charfield(choices=TYPE_CHOICES)
LOC_CHOICES = (‘Brussels’, ‘London’, ‘Dublin’, ‘Paris’)
location = charfield(choices=LOC_CHOICES)
price = PostivieInteger()
Users can select only “type”, only “location” or both (not making selection is equal to ANY) in which case I end up with 3 different filters:
Property.objects.filter(type=’apartment’)
Property.objects.filter(location=’Dublin’)
Property.objects.filter(type=’apartment’, location=’Dublin’)
The main question: django-filter the best option?
Question 1: what’s the best option of accomplishing this overall?
Question 2: how do I add model fields to the filter based on user’s form selection?
Question 3: how do I do the filter based on user selection? (I know how to use .filter(price_lt=).exclude(price_gt=) but again how do I do it dynamically based on selection as “ANY” would mean this is not included in the query)
I had a similar case like yours (real estate project), I ended up with the following approach, you can refine this to your needs...I removed select_related and prefetch_related models for easier reading
properties/forms.py:
class SearchPropertyForm(forms.Form):
property_type = forms.ModelChoiceField(label=_("Property Type"), queryset=HouseType.objects.all(),widget=forms.Select(attrs={'class':'form-control input-sm'}))
location = forms.ModelChoiceField(label=_('Location'), queryset=HouseLocation.objects.all(), widget=forms.Select(attrs={'class':'form-control input-sm'}))
Then in the properties/views.py
# Create a Mixin to inject the search form in our context
class SeachPropertyMixin(object):
def get_context_data(self, **kwargs):
context = super(SeachPropertyMixin, self).get_context_data(**kwargs)
context['search_property_form'] = SearchPropertyForm()
return context
In your actual view (I apply the search form as a sidebar element in my detailview only:
# Use Class Based views, saves you a great deal of repeating code...
class PropertyView(SeachPropertyMixin,DetailView):
template_name = 'properties/view.html'
context_object_name = 'house'
...
queryset = HouseModel.objects.select_related(...).prefetch_related(...).filter(flag_active=True, flag_status='a')
Finally your search result view (this is performed as GET request, since we are not altering any data in our DB, we stick to the GET method):
# Search results should return a ListView, here is how we implement it:
class PropertySearchResultView(ListView):
template_name = "properties/propertysearchresults.html"
context_object_name = 'houses'
paginate_by = 6
queryset = HouseModel.objects.select_related(...).prefetch_related(...).order_by('-sale_price').filter(flag_active=True, flag_status='a')
def get_queryset(self):
qs = super(PropertySearchResultView,self).get_queryset()
property_type = self.request.GET.get('property_type')
location = self.request.GET.get('location')
'''
Start Chaining the filters based on the input, this way if the user has not
selected a filter it wont be used.
'''
if property_type != '' and property_type is not None:
qs = qs.filter(housetype=property_type)
if location != '' and location is not None:
qs = qs.filter(location=location)
return qs
def get_context_data(self, **kwargs):
context = super(PropertySearchResultView, self).get_context_data()
'''
Add the current request to the context
'''
context['current_request'] = self.request.META['QUERY_STRING']
return context
Your solution works. I've modified it and I'm not using ModelChoiceField but the standard form.ChoiceField. The reason for that is that I wanted to add option "Any". My "if" statements look like:
if locality != 'Any Locality':
qs = qs.filter(locality=locality)
if property_type != 'Any Type':
qs = qs.filter(property_type=property_type)
if int(price_min) != 0:
qs = qs.filter(price__gte=price_min)
if int(price_max) != 0:
qs = qs.filter(price__lte=price_max)
if bedrooms != 'Any Number':
qs = qs.filter(bedrooms=bedrooms)
And so on....
This does the job, however it seems like an ugly and hacky solution to a simple problem. I would think is a common use case. I feel there should be a cleaner solution...
I've tried the django-filter. It is close to doing what I want but I couldn't add the "Any" choice and it filters inline rather than returning. It should do with some modifications.
Cheers

Django set form initial data in view

I'm trying to populate a django modelform with initial data provided from an external source. To achieve that I start by pull all the needed data from the external source:
url =('http://myapi.example.com')
data = urllib2.urlopen(url)
result = json.load(data)
api_data_name = result['properties']['name']
api_data_type = result['properties']['type']
Followed by populating a dict which will serve as initial data to my form:
data = {}
for field in my_model._meta.fields:
if field.name == 'name':
data[field.name] = api_data_name
form = MyEditForm(initial=data)
Then I'm passing the form to the template and the initial data is populating my text fields as expected, but now I need to be able to set a value of a select field based on a string I receive from my external source and Im not getting how can I achieve that, since doing something like:
if field.name == 'type':
data[field.name] = api_data_type
Wont do the job cause the select element has "0", "1", "2", etc as options value and not the long description i get from api_data_type variable.
How can I get the long_description from all the options <option value="1">long_description</option> of my select field in my view so i can compare each one with api_data_type?
Heres a sample of my models.py and forms.py:
#models.py
TYPE = (
('0',_(u'Type1')),
('1',_(u'Type2')),
('2',_(u'Type3')),
)
class MyModel(models.Model):
...
type=models.CharField(max_length=30,choices=TYPE,blank=True)
...
#forms.py
class MyEditForm(forms.ModelForm):
class Meta:
model = MyModel
widgets = {
...
'type': Select(attrs={'class':'select-small span2'}),
...
}
Found out how to accomplish what I asked.
# For select fields
if field.name == 'classification':
for choice in field.choices:
if choice[1].lower() == api_poi_classification.lower():
data[field.name] = choice[0]
And for any of ya trying to populate many-to-many fields (as checkboxes in my case)
# Many to many fields populate
for field in hotel_poi._meta.many_to_many:
if field.name == 'views':
if u'Vista' in api_poi_review_fields:
api_vistas = api_poi_review[u'Vista']
# The api_vistas string comes from api in the format (v1; v2; v3; v4)
views = api_vistas.split(';')
choices = field.get_choices()
temp = []
for view in views:
for choice in choices:
if view.lower().strip() == choice[1].lower().strip():
temp.append(choice[0])
data[field.name]=temp
All of this could be avoided if I had direct database access... In that case i would just need to set an object instance like m = MyModel.objects.filter(id=1) and call form = MyEditForm(instance=m)
But that was not the case and that's what makes this question a bit particular.

Django filter only if the "category" isset!

How can I do this?
I only what to filter if the request is set.
ex. if gender not set, the filter will be:
Test.objects.filter(categories=category, brands=brand)
def index(request):
gender = request.GET.get('gender')
category = request.GET.get('category')
brand = request.GET.get('brand')
Test.objects.filter(genders=gender, categories=category, brands=brand)
If you realize that filter conditions can be passed in as keyword arguments the solution becomes easier to visualize. For e.g. consider the snippet below. This is a verbose way of doing it:
conditions = dict()
for filter_key, form_key in (('genders', 'gender'), ('categories', 'category'), ('brands', 'brand')):
value = request.GET.get(form_key, None)
if value:
conditions[filter_key] = value
Test.objects.filter(**conditions)
Of course another way of doing this would be to use a form rather than pick up the values directly from the GET request.
def index(request):
gender = request.GET.get('gender', None)
category = request.GET.get('category')
brand = request.GET.get('brand')
results = Test.objects.filter(categories=category, brands=brand)
if gender:
results = results.filter(genders=gender)
If gender is not specified, gender variable will set to None, so following if gender block will not be executed.