Flask Biography Tutorial Part XII - Implementing Profile Page using Inline Editing

Utilizing X-editable to edit biography page

Utilizing X-editable to in-place editing our biography page

UPDATED: Previously I said that I use wsgi/static/upload to store user's uploaded avatar. Although it's working, it still imposed one important problem: the content of wsgi/static/upload itself will be removed for each subsequent git push command. We must store user's data into $OPENSHIFT_DATA_DIR/upload directory. How do we do this? Simple. First, (if it's currently existed) remove the folder wsgi/static/upload, and then create an executable binary script within the folder .openshift/action_hooks/post_deploy, with the content: ln -sf $OPENSHIFT_DATA_DIR/upload $OPENSHIFT_REPO_DIR/wsgi/staticgi/static/. Make sure to set the flag bit into executable using the command: chmod +x post_deploy (I don't think you can do this in Windows. *nix/OSX will do just fine for this).This script will be run each time we push the repository to Openshift --after the application successfully deployed, making a link to upload directory of user's uploaded avatar. This way, user's avatar will get stored in $OPENSHIFT_DATA_DIR/upload and will always available each time we push our application to Openshift.


As I start a new series on this blog about Python Cloud Computing and working my first way of making our Flask Biography Application runnable within PythonAnywhere, I realize that I haven't completed the last important thing for this application to be fully functional: editing biography for registered users. This biography application will become practically useless if users don't have the ability to edit their full name, tagline, avatar and short biography. Having said that, let's make it even better by implementing in-place editing for all those fields, where in-place editing simply means users will be able to edit those fields directly within the same page. We are going to use Vitaliy Potapov's X-editable for this purpose. It's an awesome Open Source product!

What is in-place editing?

In my experience, having in-place editing in a web application can enhance users experience in using our web application. In a way it's like the old WYSIWYG (but I am not sure our new generation even know that this term existed!) applied to a web page. Consider a sample biography in this page, where there are several information shown there. What if user want to change their tagline that goes beneath their name? Try to imagine two scenarios and decide what's the best:

  1. Login and then move around until user reach its setting page to finally able editing the fields that they want, save it, and then go back to biography page to see how their biography page looks like, or
  2. Login and redirect users to the same biography page but now with visual indicator that certain fields are editable, interact with it, save it and automatically present users the modified page, all without leaving that particular page.

The first approach is a well-known way of modifying data in the previous Web 1.0 (yeah, let's use this term). It's not as fun as it is in the current Web 2.0/Web 3.0 era, where users are already familiar with in-place editing. Beyond other things, your route to educate users on how to use your web application is shortened. Making your application become more user friendly and easy to use.

Prepare X-editable in your application

Go ahead to X-editable home page, have a look at the demo or directly download the Bootstrap 3 version of this wonderful plugin. It come with other bundle, which is jQuery-UI and jQuery. But as we use Bootstrap 3, let use the Bootstrap 3 version. Extract it into our Openshift's wsgi/static directory.

You will have to include X-editable javascript and css into your web application. But including these resource should take one important consideration: only include them, if user are already signed. It's useless to include them, as in-place editing feature only make sense to signed user. Here is an example of how to achieve this in our wsgi/templates/themes/water/bio.html, which use Jinja2 templating:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% extends "base-layout.html" %}
{% block header %}
{% if session['signed'] %}
<link href="/static/bootstrap3-editable/css/bootstrap-editable.css" rel="stylesheet"/>
{% endif %}
{% endblock %}
...
...
...
{% block footer %}
{% if session['signed'] %}
    <script src="/static/bootstrap3-editable/js/bootstrap-editable.min.js"></script>
{% endif %}
{% endblock %}

How to render editable fields

Let's have a look how our Jinja2 code render our user's fullname field in their bio page:

1
<h1>{{ user.fullname }}</h1>

Plain and simple.To let the same field editable using X-editable this is what we must do:

1
2
3
4
5
    {% if session['signed'] %}
        <h1><a href="#" id="fullname" data-type="text" data-url="/user_edit_fullname" data-pk="{{user.id}}"  data-title="Change your fullname">{{ user.fullname }}</a></h1>
    {% else %}
        <h1>{{ user.fullname }}</h1>
    {% endif %}

Here, we render the field using consideration that it'll displayed by signed/unsigned user. Hence, the Jinja2 {% if %} statement. Have look at our X-editable fullname field. Every attribute that begins with data-* are X-editabled required attribute, explained quickly as follows:

  • data-type : input field available as editing control for this particular field.  In this article we only need to use text and textarea. Consult the documentation for complete list of editing control there available. PS: Actually, we need another image uploader field. But currently X-editable don't support them, so we have to work our away around that.
  • data-url : URL being called when user finished editing field. Of course X-editable doesn't impose certain technology to be use as the back-end server. As long as it's able to receive POST message and return JSON response, all server side technology are welcome. Another thing to note is, the X-editable will only access this URL if you change the value. So, if you merely click it and then pressing Enter without changing anything, X-editable won't access this URL (which is only logical). Remember this behaviour, or you'll scratch  your head tying to understand why it didn't access this URL.
  • data-pk : primary key for this particular data
  • data-title :title displayed in the popup window when editing field.

X-editable also support inline editing, where there is no popup dialog displayed. But, I prefer the popup style, which is the default option in X-editable.

Only composing the field using the above attribute won't convert an input field into X-editable one. You will have to call editable() function for each input field that will be designated as X-editable fields. Below are complete JavaScript code use to convert all regular input fields into X-editable one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
        $('#fullname').editable({
            placement: "right",
            error: function (errors) {
            }
        });
        $('#tagline').editable({
            placement: "right",            
            error: function (errors) {                
            }
        });
 
        $('#biography').editable({
            placement:"right",
            error: function (errors) {
            }
        });
</script>

Placement attribute will specify position to display popup editing window relative to its field. Error attribute is use to define callback function if the call to data-url returning an error result. Maybe you were asking, "What about success attribute?". Indeed success function handler will be call if the call to data-url returning a success result. But, in my testing, the default success function handler is already sufficient enough. It gives visual feedback to user (yellow fading light in successfully edited field) that pleasing and reassuring about successful editing operation.

What if you omitted the error callback? Well, it's not a pleasant view if error indeed occurred, as this image depicted:

Not specifying error callback in X-editable

Not specifying error callback in X-editable

Implementing Back-End Processing

Now that our front-end is ready, we must prepare the back-end part. Without further ado, here are all the back-end part that responsible for editing fullname, tagline and biography field:

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
@application.route('/user_edit_fullname',methods=['GET', 'POST'])
def user_edit_fullname():
    id = request.form["pk"]
    user = Users.query.get(id)
    user.fullname = request.form["value"]
    result = {}
    db.session.commit()
    return json.dumps(result) #or, as it is an empty json, you can simply use return "{}"
 
@application.route('/user_edit_tagline',methods=['GET', 'POST'])
def user_edit_tagline():
    id = request.form["pk"]
    user = Users.query.get(id)
    user.tagline = request.form["value"]
    result = {}
    db.session.commit()
    return json.dumps(result)
    
@application.route('/user_edit_biography',methods=['GET', 'POST'])
def user_edit_biography():
    id = request.form["pk"]
    user = Users.query.get(id)
    user.bio = request.form["value"]
    result = {}
    db.session.commit()
    return json.dumps(result)

If you have already reach this series until this step, I am sure the above code will be self-explanatory, as it use our familiar SQLAlchemy object.

For the return part itself, X-editable expect a JSON result for the response of this back-end part of the application. Here, I simply return an empty JSON data for a successful execution of data updating."What if error occurred such as database failure?". Don't worry. The default behaviour of X-editable is good enough for this purpose: it will give red error-alert outline around editing control, and won't close the popup window. User would have to explicitly cancelled the save operation of this particular input field.

An error occurred in the back-end part. You will have to cancel this popup window.

An error occurred in the back-end part. You will have to cancel this popup window.

And that's all! User now will be able to in-place edit all of our defined biography fields.

... all?

Wait, we still have to allow users to upload their own avatar! 

Implementing Image Uploading Feature

Actually, it'd be great if X-editable also support image uploading. Unfortunately, currently it's not. But, now that we already familiar with Bootstrap+jQuery and already know how to display and use modal window, implementing a basic image uploader should not be too hard. Let see how we tackle this issue of uploading image using Flask in Bootstrap + jQuery application. 

First, let see how we render the image that will allow the same level of in-place editing as our previous input fields:

1
2
3
4
5
{% if session['signed'] %}
                <a href="javascript:uploadAvatar({{user.id}});"><img id="avatar" width="300" class="img img-rounded" src="{{ user.avatar }}"></a>
                {% else %}
                <img id="avatar" width="300" class="img img-rounded" src="{{ user.avatar }}">
            {% endif %}

Instead of the regular <img/> element for avatar, we wrap it into a hyperlink that point to a javascript function call uploadAvatar() that send user.id  as its parameter, to allow updating user avatar when the upload process completed.

1
2
3
4
5
function uploadAvatar(id)
        {
            $("#avatar_user_id").val(id);
            $('#avatar_form').modal();
        }

In the above JavaScript code, we save the value of user.id into a hidden field named avatar_user_id , which later will be send to Flask back-end image uploading method. This javascript function will result in displaying an upload file modal dialog as seen below:

Upload avatar window displayed as popup modal dialog

Upload avatar window displayed as popup modal dialog

 

Although it's pretty neat, at the current application state I haven't add image previewing, upload progress bar and image resizing feature in this image uploader utility. It still basic, but it's already suffice its purpose in giving user a nice experience with in-place editing. To complete the explanation, below is the code for the above dialog window:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="modal fade" id="avatar_form" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
        <h4 class="modal-title" id="myModalLabel">Upload Avatar</h4>
      </div>
      <div class="modal-body">
          <form id="upload-file" method="post" enctype="multipart/form-data">
              <input type="hidden" id="avatar_user_id" name="avatar_user_id"/>
            <span class="btn btn-success fileinput-button">
            <i class="glyphicon glyphicon-plus"></i>
            <span>Add files...</span>
                <input name="file" type="file">
            </span>
            <button id="upload-file-btn" type="button" class="btn btn-primary">Upload</button>
        </form>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
      </div>
    </div>
  </div>
</div>

After user choosing a file, the function in upload button was called:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
        $(function () {
            $('#upload-file-btn').click(function () {
                var form_data = new FormData($('#upload-file')[0]);
                form_data.append("avatar_user_id", $("avatar_user_id").val());
                $.ajax({
                    type: 'POST',
                    url: '/user_upload_avatar',
                    data: form_data,
                    contentType: false,
                    cache: false,
                    processData: false,
                    async: false,
                    success: function (data) {              
                        $('#avatar_form').modal('hide');
                        $("#avatar").attr("src", data);
                    },
                });
            });
        });
    </script>

We use jQuery ajax() function to send the choosen file into the back-end Flask method. Notice how we also append user.id  into the FormData instance. This will allow the back-end Flask method to update user avatar to the path of uploaded image. 

What about the Flask back-end method itself? Here it is (credit goes to this concise gist flask file upload example):

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
from werkzeug.utils import secure_filename
 
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.realpath(__file__)), "static/upload")
ALLOWED_EXTENSIONS = set(['bmp', 'png', 'jpg', 'jpeg', 'gif'])
application = Flask(__name__)
application.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
 
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
 
@application.route('/user_upload_avatar', methods = ['POST'])
def user_upload_avatar():
    if request.method == 'POST':
        id = request.form["avatar_user_id"]
        file = request.files['file']
        if file and allowed_file(file.filename):
            user = Users.query.get(id)
            filename = user.username + "_" + secure_filename(file.filename)
            file.save(os.path.join(application.config['UPLOAD_FOLDER'], filename))
            img = "/static/upload/" + filename
 
            user.avatar = img
            db.session.commit()
            return img

 

In implementing file upload feature in any web application, the most important thing that must be consider first, is the location of the uploaded files. We must make sure that the directory is writable and accessible. Thinking in term of Openshift directory structure, I choose to store all uploaded images into /static/upload directory. After testing the application live, I can report that users can upload image into that directory and as it's within /static path, the uploaded images are easily accessible. Also note also how the uploaded image are given a unique name: prefixing the filename with its owner name, is adequate in this case, as the username itself is unique.

After the image finally saved, the user avatar field was updated and the full path of this newly uploaded image was sent back to the caller, where it will be use to dynamically change avatar src attribute, creating an effect of in-place editing as previously mentioned.

What's Next?

You know, thinking of making a software perfect, directly starting an endless wave of flows of ideas and inspirations. If  time is infinite, we can be sure that the works of making a software perfect is also infinite. But, everything must eventually come to an end, due to time constraint, budget and other aspects. This Flask Biography Application is one example of it. I start this series with the end in mind, that it must be serve as a not so typical Flask tutorial application where you were presented with a simple blog application (although, having rethink of it, blog can also be made as complex as possible. No limit there!). I do have a high hope that this series will be valuable to readers out there who finding it hard to start a new Python web applicatin project using Flask. 

.... well, actually it's not a good bye for this series :)

I have one important thing left : structuring this project in a way that it will be able to serve a larger Flask web application project development. You don't think I will left you with a Flask project that contains only within a single file main.py don't you? Cool

As always, you can download the part 12 of this application here : bio-part-12.zip.  

Or follow its Github repository here : pythonthusiast/bio.

Stay tuned!

 




Leave comments

authimage

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