Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic field filter in select2 #29

Closed
philliproso opened this issue Jul 18, 2014 · 25 comments
Closed

Dynamic field filter in select2 #29

philliproso opened this issue Jul 18, 2014 · 25 comments

Comments

@philliproso
Copy link

Maybe I am not really understanding edit_form_query_rel_fields, but this is a pretty important feature. Build a filter to the drop down list based on the selection of previous drop down lists.

Usecase user select category A from form field 1. User can then only select subsets of category A from form field 2.

Is there an easy way of doing this

PS 0.10 looks rad

@dpgaspar
Copy link
Owner

The usecase you describe does not exist. You can only filter select fields individually.

Your right this could be an interesting feature....

To develop this you must develop the template yourself. Then override the edit_template property.

@philliproso
Copy link
Author

I might give it bash. A concrete example is some-one needs to add a new item to a store. They first select an item group say fruits, then select an actual item which then gives a drop down list of apple, banana etc. They then add quantity price etc and submit.

Currently one of the ways to do this is to overload the repr_ of an model to be '%s -%s' %(self.group,self.name) so that a user can manually add the filter in the select2 text box by typing the group, essentially duplicating inputs but still not having to scroll down a long list.

Once you have cascading filters based on inputs you can start doing really fancy stuff like forcing users to only select items that are part of a particular hierarchy all on one simple form.

@dpgaspar
Copy link
Owner

Yes, this is a recurring problem/feature when developing a web app.

One way to do it: when the user selects a 'master' combo the form reloads (or submits to a specific endpoint) the server populates the slave combos and renders the form again.

Another way: when the user selects a 'master' combo fires something like jQuery.getJSON() to a specific endpoint the server returns Json data for the slave combo(s).

What would be the best way to define an API for this feature? regarding of course the current API for ModelView or extending Model from SqlAlchemy. Any ideas ?

@philliproso
Copy link
Author

Option A: Clean less flexible relies on metadata look ups to determine fk and many to many etc.

add_form_cascade_query=[('field_name',['parent1_field_name','parent2_field_name'])]

Option B: Flexible, similar to existing query API

add_form_query_cascade = [('field_name', 'parent field'
SQLAModel,
[['column of related object',FilterObject,'parent field's object's column name']]
)]

Here you should be able to specify multiple field names and a field name multiple times.

@dpgaspar
Copy link
Owner

good suggestions, need some time to think about it

@philliproso
Copy link
Author

I just had a bit of a rethink about this API and the API for filters in general. I came up with the following: Each filter is simply function that accepts a query and then returns a query.

Eg: lambda query: query.join(Mod).filter(Mod.id==55)

Then the api for the cascading filter could just be a function that accepts the current state of the form and a query object and returns a query object.

Eg: lambda query,item: query.join(SomeMod).filter(SomMOd.name=item.som_mod.name)

This api could then technically apply to all filters and user just needs understand the principal of one API.

@philliproso
Copy link
Author

My last comment feels kinda retarded seeing that the basefilter works this way.

@dpgaspar
Copy link
Owner

It's not retarded at all, actually it could be a good idea (it would make a 'cleaner' API).
But it would not be possible (or easy) to implement a solution for searching on lists so dynamic like it is today.
Also, I would like to support other kind of data engines (NoSQL) in the future, this solution makes an abstraction layer that make it possible.

Never feel bad to propose, question or brainstorm, even if you get it wrong. Actually your comments made me think about supporting model joins, and question about the API in general.

@ben-github
Copy link
Contributor

I don't have much to add, but would enjoy this feature as well. For my purposes, here's the work-around I did.

Table A has two columns that are a composite Foreign key to two columns in Table B. When adding an entry to Table A, the two foreign columns are treated independently by Flask-AppBuilder (using somewhat tricky relationships in the model.py definition) so can select
("X,only-works-with-Y") instead of ("Y,only-works-with-Y"). But when you do the insert, the db foreign key rejects what doesn't work so at least you know -- hopefully the end user knows what combination are valid. Not ideal but functional.

@philliproso
Copy link
Author

I noticed their was some work already done on this.
I have taken another stab at this and realized there would need to be some larger changes. Namely select2 fields would get all their data via ajax.
The filled in form is serialized and passed to the ajax request so that it can be used in the query.
This means the cascade filter can be added. Plus a user can overload the ajax request method and do whatever they want with it.
I will make a pull request when I done.

@dpgaspar
Copy link
Owner

Careful with turning all select2 to ajax, search widget uses it. This is a tricky one...

@philliproso
Copy link
Author

Yes it is turning out to be rather tricky. I have the base set up, But now trying to make sure all those self.add_form.refresh calls don't reset the dynamically set form choices not allowing the form.validate() to succeed. Plus ensuring all the existing base filters work. While trying to continue working only through the datamodel and filters api to ensure everything else works for other db's.

@dpgaspar
Copy link
Owner

If you need any help, don't hesitate.

@philliproso
Copy link
Author

Sorry for taking long. I have this feature for the standard ModelViews. But the search widgets are still a problem. What is the best approach to fixing this is?
If you want you can see the changes in my fork.
https://github.com/philliproso/Flask-AppBuilder/tree/ajax_select_2

@dpgaspar
Copy link
Owner

No problem

Lot of work here, with some major changes. I need to carefully look at this...

@philliproso
Copy link
Author

Looking back, I think it is best to simply show an example of how to create this feature using a custom widget, an additional form field, overloading the pre_add method and the add and edit template.

This would have minimum impact on the existing code base. Currently what I have done in my fork is a bit too disruptive. However I think long term all select 2's should be moved to ajax style requests, and the filters module could probably do with some simplification.

@dpgaspar
Copy link
Owner

Looks great could you write a little tutorial on the docs about this?

@philliproso
Copy link
Author

I will create an example and make a pull request just for the example

@dpgaspar
Copy link
Owner

Great, Thanks.

@scheung38
Copy link

This talks about it here

http://librelist.com/browser/flask/2012/10/3/populating-forms-based-on-initial-entry/#00bf3feb9757706a4254b0e3596a2159

But I am not sure if compatible with Flask-AppBuilder

@jvinolas
Copy link

jvinolas commented Apr 2, 2015

And with a little of ajax? You have to expose /filtrarplantilla/
(haven't test it in appbuilder, but works with Flask)

function filtrarPlantillaTipus(element) {
    var tipus = element.options[element.selectedIndex].value
    var llista = document.getElementById("VMTemplate")
    $.ajax({
        type: "GET",
        //~ dataType: "json",
        url:"/filtraPlantilla/" + tipus,
        success: function(templates)
        {
            if (templates.length >0) {
                var contingut='<option value="">Seleccioneu una entre '+templates.length+' plantilles trobades</option>';
            } else {
                var contingut='<option value="">Cap plantilla coincident</option>';                     
            }
          for (var i=0; i<templates.length; i++) {
                contingut=contingut+'<option value="'+templates[i]['id']+'" >'+templates[i]['name']+'</option>';
            }
          llista.innerHTML=contingut;
        }               
    });

And then in your template:

            <tr>
                <td>Tipus de plantilla:</td>
                <td><select name="VMTemplateType" onchange="filtrarPlantillaTipus(this);">
                        <option value="">Seleccioneu tipus de plantilla:</option>
                        <option value="VIMET">vimet</option>
                        <option value="MEVES">Meves</option>
                        <option value="PUBLIQUES">Públiques</option>
                        </select>
                </td>
            <tr>
                <td>Plantilla:</td>
                <td><select id="VMTemplate" name="VMTemplate" onchange="selectChange(this);">
                        <option value="">Seleccioneu plantilla base:</option>
                </td>
            <tr>

@jvinolas
Copy link

jvinolas commented Apr 2, 2015

Hi again,

One option will be to override the add.html template to add your own javascript code and find what is the id for the html select block. For example:

                <select class="my_select2 form-control" data-placeholder="Select Value" id="template_base" name="template_base" style="width:250px"></select>

That in your example will be dummy_city2 or something like that.

But you should call filtrarPlantillaTipus on the first select (location) so I think the only way will be to create your own select widget that calls that function onchange.

This is only an idea.

@scheung38
Copy link

Hi Jvinolas,

I have overriden add.html in my views.py:

class MyGeneralView(ModelView):
datamodel = SQLAInterface(MyDataBase)
add_template = 'add_contacts.html'

Then in my templates/add_contacts.html:

{% extends "appbuilder/general/model/add.html" %}
{% block add_form %}
{{ super() }}
"""

Welcome

This Text is before the add form widget
"""
{% endblock %}

But trying to work out how to do what you suggested.

BTW is your syntax above correct? tr don't seem to have closing tags?

And how to expose /filtrarplantilla/? I do have Flask-Restless so maybe in init.py:

api_manager = APIManager(app, flask_sqlalchemy_db=db)
api_manager.create_api(Countries, methods=['GET', 'POST', 'DELETE', 'PUT'])

create one more for localhost:8080/api/filtrarplantilla/
api_manager.create_api(xxx?, methods=['GET', 'POST', 'DELETE', 'PUT'])

And where to place that statement:

select class="my_select2 form-control" data-placeholder="Select Value" id="template_base" name="template_base" style="width:250px">

@scheung38
Copy link

http://devzone.co.in/simple-example-of-dependable-dropdowns-cascading-dropdowns-using-angularjs/

Also, doesn't seem to work above angularJS > 1.0.8 when current stable release is 1.3.15.

in views.py:
class MgntServerGeneralView(ModelView):
datamodel = SQLAInterface(MyTable)
add_template = 'testDropDown.html'

/templates/testDropDown.html:

<title>Cascading Dropdowns in AngularJs :devzone.co.in </title> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script> <script> function CountryCntrl($scope) { $scope.countries = { 'India': { 'Maharashtra': ['Pune', 'Mumbai', 'Nagpur', 'Akola'], 'Madhya Pradesh': ['Indore', 'Bhopal', 'Jabalpur'], 'Rajasthan': ['Jaipur', 'Ajmer', 'Jodhpur'] }, 'USA': { 'Alabama': ['Montgomery', 'Birmingham'], 'California': ['Sacramento', 'Fremont'], 'Illinois': ['Springfield', 'Chicago'] }, 'Australia': { 'New South Wales': ['Sydney'], 'Victoria': ['Melbourne'] } }; } </script>
    <div ng-controller="CountryCntrl">
        <div>
            Region:
            <select id="country" ng-model="states" ng-options="country for (country, states) in countries">
                <option value=''>Select</option>
            </select>
        </div>
        <div>
            Cities: <select id="state" ng-disabled="!states" ng-model="cities" ng-options="state for (state,city) in states"><option value=''>Select</option></select>
        </div>
    </div>

</body>

in app.js:

function CountryCntrl($scope) {
$scope.countries = {
'India': {
'Maharashtra': ['Pune', 'Mumbai', 'Nagpur', 'Akola'],
'Madhya Pradesh': ['Indore', 'Bhopal', 'Jabalpur'],
'Rajasthan': ['Jaipur', 'Ajmer', 'Jodhpur']
},
'USA': {
'Alabama': ['Montgomery', 'Birmingham'],
'California': ['Sacramento', 'Fremont'],
'Illinois': ['Springfield', 'Chicago']
},
'Australia': {
'New South Wales': ['Sydney'],
'Victoria': ['Melbourne']
}
};
}

But this doesn't tie in with Flask-AppBuilder SQLAlchemy with look and feel and database.

@scheung38
Copy link

So is it a challenge to implement in Flask-AppBuilder? Or is it quite straight forward to add, say AngularJS logic to the front end by overriding select widget?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants