Flask Biography Tutorial Part X : Building Portfolio Form Modal Dialog using Flask-WTF, Bootstrap 3, Bootbox, Bootstrap Tags Input and jQuery

 

 

I got to admit something : I really have fun writing this current article! It shows concise way for new comer in Python web development area to truly understand how to integrate Bootstrap and its extensions, jQuery and Flask to build feature that increase user experience in interacting with the application.

Previously we haven't utilize jQuery and many Bootstrap extensions there exist in the web, making our application solely depends on what Flask community gave us (which is great!). But, as we already choose to leverage Bootstrap 3 in our application, this decision bring great advantage : we can easily tap to plethora of Bootstrap extension built by community and add it in our own application to gain benefit of its functionality. Specifically we are going to maximize the use of this open source products to our application:

  1. jQuery, especially its .getJSON and .post method to allow our Portfolio form live as a modal dialog box complete with Ajax validation and Ajax form submission. I got to beg you pardon for this, for thinking in the previous post that we have to use jQuery client side validation. Turn out what I really want is an Ajax based validation technique. I will explain later what are the differences. But one important thing is, we don't have to double our validation mechanism complexity by also implementing it in client-side.
  2. Bootstrap Tags Input, making entering tags as comma separated value is a pleasant experience.
  3. Bootbox that lets our application utilize a popup confirmation dialog easily.

Ready? Let's start!

Previously  we have shown how our portfolio form shown as a modal dialog box, meaning user must work on this form without him/her being able to work on another part of the system. But our previous modal dialog box was still dumb : it just popup without any data there. And being closing the form, of course there is new data added or existing data edited. Here, we will enliven the form by clearing the input fields if users were about to add a new data or filling it with relevant data if users were about to edit an existint data. We will also work on how to do form validation and form submission without closing this form, which is a must in a modal dialog box.

Figure X1 above shows our portfolio modal dialog box with error validation in its description field. User can't just easily press submit button to close this dialog box. (S)he must carefully enter valid input to be able to close this dialog box and save its input data. Otherwise, (s)he can also press Cancel to cancel the data input process. This, making a truly save data entry process : you will not get an invalid data.

But how do we implement this interesting feature? Isn't that the data will only get processed if our application request new page to the server and displaying the result in a whole new page? Well, it's if you live on the 90s...

A. Re-introducing Web 2.0 Paradigm

A1. Ajax to The Rescue!

Historically, the term Ajax was coined in 2005. Simply put, it's the technique of web development where request to the browser was done in the background by means of XMLHttpRequest JavaScript object. Prior this technique was introduced, people on the world (who is using the internet to be exact) already been use to have the browser refresh the whole page just to see some tiny bit of update on their current page. All of the time it's a must. But sometimes, it's an ineffective way to interact with the web. 

At the early development of Ajax, cross browser compatibility is a main concern. The same Ajax call may use slightly different implementation that create a headache for web developers. Thanks to two jQuery methods .getJSON(to use Ajax call with GET request) and .post(to use Ajax call with POST request)  we can be sure that the same call will work the same whether users use FireFox, Opera, Chrome or any other web browser on the planet (hopefully). This kind of background processing technique become one factor that generates a new wave of internet application: The Web 2.0.

In our Bio Application, we are going to use both of these methods to create a working dialog using Ajax processing.

A2. JSON to Standardize Data Format in Ajax Processing

JSON is yet another great invention in our long history of internet development. Started as a means for stateful, realtime server to browser communication without the need for another browser plugin such as Flash or Java applets, now JSON is an integral part of any Ajax processing. Prior to JSON there was XML. But, in the latest development, developer tends to rely on JSON to do server-browser communication using Ajax. Mainly because it's easier and leaner, and also because JSON structure can be mapped directly to programming languages data structure, which is not the case with XML. In Python, you can see that JSON structure can be mapped directly to Python dict

In our Bio Application, we will use JSON intensively in Ajax processing to communicate update,  call result and any data query result between server-browse and vice versa. You will find that Python simplicity in handling JSON is somewhat an aww experience..

B. Displaying Modal Portfolio Form

First, recall that we are about to show this Portfolio Form as a modal dialog whether users about to Add a new portfolio o Edit an existing one. An add action will simply reset our portfolio form while an edit action will fill our portfolio form with existing portfolio data. We will also use Bootstrap Tags Input to bring a better experience to our users in editing tags field. 

B1. Using Bootstrap Tags Input to Implement Tags Editing

The term tagging was also coming from Web 2.0 paradigm. It's a non-hierarchical way to categorize content based from simple text, usually separated by comma or space. Entering tags has become very common, so that Bootstrap and jQuery community has a lot of tags editing input plugin for it. Here, I am going to use Bootstrap Tags Input, which is a jQuery plugin that use Bootstrap styling to turn regular input element into a cool and interactive tags editor. Figure X1 also shows the result of using Bootstrap Tags Input in our tags field.

To use this plugin is very simple :

  1. Download its zip file,
  2. Include  its bootstrap-tagsinput.js and bootstrap-tagsinput.css into our bio.html plugin
  3. Add the attribute data-role=tagsinput into your input field

bootstrap-tagsinput.js will automatically queried all element having the attribute data-role=tagsinput and turn it into tags editing field by calling tagsinput() method. So, practically that's are the only thing that you need to do.

But, as we are displaying tags field in a modal dialog box for adding new data and edit existing data, we must initialize our tags field differently, which will be explained in great detail in the subsequent sections.

B2. Add new portfolio

Our add action was triggered by our green Add button defined below:

1
<button class="btn btn-default btn-sm btn-success" data-toggle="modal" data-target="porto_form" id="portform_btnadd">Add</button>

This is suffice to display a non-database related modal dialog box, as there is no data need to be prepared. But as we need to prepare the form to insert new portfolio data, we need to add extra jQuery code.

1
2
3
4
5
6
7
8
$("#portform_btnadd").click(function(){
        $("#portfolio_id").val("");
        $("#title").val("");
        $("#description").val("");
        $("#tags").tagsinput("removeAll");
        $("#porto_form").modal('show');
        return false;
    });

I believe this is the first time in my blog where I show a jQuery code. Let me guide you step by step on this simple jQuery code. This  code was entered between <script></script> element, which eventually enclosed  inside {% block footer %} in our bio.html template, so the script was processed after any other javascript library loaded. This will avoid error where we try to define jQuery method without jQuery itself has been loaded.

Our code will basically define a function for click event  for an element with id  portform_btnadd, which is our Add new portfolio data. As we prepare our form for new portfolio data, you can inspect that LINE 2 ~ LINE 5 will simply clear any existing data in the input fields (this happens, if previously we edit an existing portfolio data) using jQuery .val() method. Special note for LINE 5 though, to clear its value we use .tagsinput() method as we use Bootstrap Tags Input for tags editing. 

After all input fields cleared, we can safely display our portfolio form using Bootstrap extension to jQuery using modal() method.

B3. Edit Existing Portfolio

Our edit action (and also delete action) was triggered by link decorated with Bootstrap glyphicon as follows.

Edit and delete button in the portfolio title

Edit and delete button in the portfolio title

 

Of course this link will only be shown for signed users. And this is the code for that edit link

As you has just read about how to display a blank portfolio form, I think you can guess what's coming. Here you go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function editPortform(id)
    {
        $("#portfolio_id").val(id);
        $.getJSON(  
            "/portfolio_get/" + id,  
            {}
        ).done(function(data){
            $("#portfolio_id").val(data.id);
            $("#title").val(data.title);
            $("#description").val(data.description);
            $("#tags").tagsinput("removeAll");
            $("#tags").tagsinput("add", data.tags);
            $("#porto_form").modal('show');
        }).fail(function(data,textstatus,xhr){
            alert('fail = ' + data + ', textstatus ' + textstatus );    
        })
        ;
    }

 

Did you expect this code? Great if you did, no problem if you didn't. Laughing

LINE 8 ~ 12 was having the same function as our Add action previously : setting fields value. But this value coming from the result of jQuery .getJSON() method call. This method is a clear example how JSON format shaped Ajax communication in web development methodology. getJSON() will run in the background and use GET request to call URL passed in its argument. Upon returning, its expect the code to return a JSON object in its data variable. Well, of course you can return anything. Or not returning anything at all. But, let us stick to the guideline, and return a plain JSON object. But, how do we do that in our Python application?

In the code above we can see that .getJSON() method will call our url portfolio_get, and here is the code for that route/view method.

1
2
3
4
5
@application.route('/portfolio_get/<id>')
@login_required
def portfolio_get(id):
    portfolio = Portfolio.query.get(id)
    return json.dumps(portfolio._asdict())

We can easily saw that this method main intention was to query Portfolio data having specific primary key. But afterward, we return its JSON format by using  json.dumps() method, which will convert Python dictionary object into JSON. 

And here comes the interesting part. An SQLAclhemy query result is not a Python dictionary object. Hence, we add _asdict() method to convert our SQLAlchemy object into Python dict. Here is the new method in our Users class.

1
2
3
4
5
def _asdict(self):
        result = OrderedDict()
        for key in self.__mapper__.c.keys():
            result[key] = getattr(self, key)
        return result

With these setup, a call from  getJSON() method to our portfolio_get route/view method will return a valid string in JSON as expected by .getJSON() method, so we can easily access its field by simply using dot operator (data.id,data.title and data.tags) and use it to initialize our modal dialog Portfolio form.

This step complete our code of showing up our portfolio form in Add new data or Edit existing data mode.

C. Implementing Ajax Validation and Ajax Form Submission using jQuery and Flask-WTF

We are about to save a portfolio form data. Which method that we need to use?

Correct. .post() method. 

Up until this article series, we had familiarize ourselves of processing form data using route/view method that will eventually call render_template function.  And actually, that's the only main different of implementing a route/view method that will serve as an Ajax post processing. All the other details are the same. Have a look at its code here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 def portfolio_add_update():
    form = PortoForm(request.form)
    if form.validate():
        result = {}
        result['iserror'] = False
 
        if not form.portfolio_id.data:
            user = Users.query.filter_by(username = session['username']).first()
            if user is not None:
                user.portfolio.append(Portfolio(title = form.title.data, description = form.description.data, tags = form.tags.data))
                db.session.commit()
                result['savedsuccess'] = True 
            else:
                result['savedsuccess'] = False
        else:
            portfolio = Portfolio.query.get(form.portfolio_id.data)
            form.populate_obj(portfolio)
            db.session.commit()
            result['savedsuccess'] = True
            
        return json.dumps(result)
 
    form.errors['iserror'] = True
    return json.dumps(form.errors)

If you need to compare it with our conventional post route/view method, please do so. You can see the main difference is that here, we use json.dumps() in several places when we want to return result of our method processing. json.dumps() is another time saver method in Python where we can convert a dict data type directly into a valid JSON format.

How do we call this route/view method using jQuery .post() method?

Well, first recall that the portfolio form is a modal dialog form, so let's inspect how we build the form and its save button:

1
2
3
4
5
6
<form class="form-horizontal" action="" method="POST" id="portform">
...                                 
                <button type="submit" class="btn btn-primary btn-success" id="portform_btnsave">Save</button>
                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
...
</form>

 

We omitted the form action attribute. But how do the submit button know how to process this form? Well, here goes the jQuery part of the solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
$("#portform_btnsave").click(function(){
        $.post(  
            "{{ url_for('portfolio_add_update') }}",  
            $("#portform").serialize(),  
            function(data, textStatus) {
 
              var errors = $.parseJSON(data);
                $("#error_title").text("");
                $("#error_description").text("");
                $("#error_tags").text("");
    
                if(errors.iserror)
                {       
                    if(errors.title!=undefined) $("#error_title").text(errors.title[0]);
                    if(errors.description!=undefined) $("#error_description").text(errors.description[0]);
                    if(errors.tags!=undefined) $("#error_tags").text(errors.tags[0]);
                }else if (errors.savedsuccess)
                {
                    $("#porto_form").modal('hide');
                    location.reload();
                }
            }  
        );
    });
</script>

Inspect that in LINE 2, we define a function for click event specifically for an element with id  portform_btnsave , which is our submit button. And when click event did occurred, in calling .post() method in LINE 3 we construct a valid string in standard URL-encoded notation for our portfolio form using jQuery serialize() method in LINE 5. This will allow our form data get processed with our route/view method as it's sent by browser using conventional POST method. 

Upon returning form the call of our route/view method, the process start by parsing the JSON result with .parseJSON() method in LINE 8. This method will generate a plain JavaScript object ready to be evaluated by our JavaScript code. We check for whether there is error in it. And if it does, we display it in the correct error block in our bio.html using jQuery .val() method.

If there is no errors, which means a new portfolio data already entered into the database, we simply hide our dialog box in LINE 26 and then reload our current page in LINE 27.

And, yes, you were right. What is the meaning of having a form submitted using Ajax, if afterward we simply reload our page to get the latest data. I think I am going to let this as exercise to the reader or will simply implement it in the upcoming article

D. Popup Confirmation for Portfolio Deletion using Bootbox

Whenever signed users trying to delete a portfolio, (s)he will be prompted with a nice confirmation dialog sliding from the top. We simply use a nice Bootstrap 3 extension for this : Bootbox.js.

Popup confirmation dialog before deleting a Portfolio

Popup confirmation dialog before deleting a Portfolio

To bring this dialog, we start it with a hyperlink to a javascript method deletePortfolio().

1
<a href="javascript:deletePortfolio('{{porto.id}}');"><span class="glyphicon glyphicon-remove"></span></a>

I am sure you are getting familiar with the above code. So, here goes the deletePortfolio():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function deletePortfolio(id)
    {
        bootbox.confirm("<strong>Portfolio deletion</strong>You can't undo this action. Are you sure?", function(result){
            if(result)
            {
                 $.getJSON(  
                    "/portfolio_delete/" + id,  
                    {}
                ).done(function(data){       
                    location.reload();
                }).fail(function(data,textstatus,xhr){
                    alert('fail = ' + data + ', textstatus ' + textstatus );    
                })
                ;
            }
        });
    }

We simply call Bootbox confirm() method to display popup confirmation dialog, with our callback function in its second argument, which will be call if users choose OK in the confirmation dialog. In there, we, guess what, simply use our old pal jQuery getJSON() method to background call all our portfolio_delete route/view method. And here goes our Flask route/view method:

1
2
3
4
5
6
7
8
9
@application.route('/portfolio_delete/<id>') 
@login_required
def portfolio_delete(id):
    portfolio = Portfolio.query.get(id)
    db.session.delete(portfolio)
    db.session.commit()
    result = {}
    result['result'] = 'success';
    return json.dumps(result)

Nothing fancy :)

Although, I omitted the case where the delete process in LINE 5 may generate an exception (due to server failure etc.). Let's see what you can come up with!

D. Conclusion

We have gone a long way to reach this step. And I am pretty sure this long series of Flask tutorial will give you a good start in trying to build a real world application. There are some several enhancements that you can add to the current application, which are:

  1. Instead of using a plain Bootstrap 3 list-group you can harness jQuery Data Tables plugin for this.
  2. Or if using Data Tables seems overkill (as it may fit more to admin interface), you can still add Boostrap 3 Pagination component, to divide our users long list of Portfolio into nice multiple pages. You may implement the page navigation using Ajax by implementing  another route/view method that will generate JSON object of users Portfolio data or simply navigate users to new page.
  3. Implement filtering function of users Portfolio by its tags.
The third step will be implemented, after I present the article of database migration. This is the last step in making our application become fully functional. Do you remember that, until now, our application will still drop and recreate all the tables before server started? This approach is intended for you to focus first on application features implementation, instead of its database schema preparation. Now that our application getting close to its destination, we will create a production quality data migration technique, so that our users can safely registered to us, although our application was still undergo great development changes.

As usual you can download the current application progress in : bio-part-10.zip.

And test the latest application live at http://bio-ekowibowo.rhcloud.com




Leave comments

authimage
  • ok

    • ok

Copyright(c) 2014 - PythonBlogs.com
By using this website, you signify your acceptance of Terms and Conditions and Privacy Policy
All rights reserved