Mega-Flask Tutorial, Part 6: Profile Page and Avatar

This is the sixth article in a series where I will document my experience writing a Python web application using the Flask microframework.

The purpose of this guide is to develop a fairly functional microblogging application, which I decided to call microblog for a complete lack of originality.



Brief repetition

In the last article, we created an authorization system, now users can log in to the site using OpenID.

Today we will work with the user profile. First, create a profile page on which information about the user and his posts will be displayed, we will also learn how to display an avatar. And then we will create a form for editing personal data.

Profile Page.

In fact, creating a profile page does not require any new concepts. We just create a new view and an HTML template for it.

Function in view. (file app.views.py ):

@app.route('/user/')
@login_required
def user(nickname):
    user = User.query.filter_by(nickname = nickname).first()
    if user == None:
        flash('User ' + nickname + ' not found.')
        return redirect(url_for('index'))
    posts = [
        { 'author': user, 'body': 'Test post #1' },
        { 'author': user, 'body': 'Test post #2' }
    ]
    return render_template('user.html',
        user = user,
        posts = posts)

The app.route decorator will be slightly different from the ones we used.
The method has a parameter named nickname . You also need to add a parameter to the view function with the same name. When the client requests the URL / user / miguel , the function in the view must be called with the parameter nickname = 'miguel' .

The implementation of the function should go without surprises. First, we will try to load the user from the database using the nickname which we took as an argument. If this does not work, then we will redirect to the main page with an error message, just as we did in the previous chapter.

As soon as we have a user, we callrender_template , along with a test message. I draw your attention to the fact that only messages from this user should be displayed on the user’s page, so you need to fill in the author field correctly .

Our initial template looks simple enough (file app / templates / user.html ):


{% extends "base.html" %}
{% block content %}

User: {{user.nickname}}!


{% for post in posts %}

{{post.author.nickname}} says: {{post.body}}

{% endfor %} {% endblock %}

We are done with the profile page, but there is no link to it anywhere. To make it easy for the user to get to their profile, we will add a link to it in the top navigation bar (file app / templates / base.html ):

Microblog: Home {% if g.user.is_authenticated() %} | Your Profile | Logout {% endif %}

Please note that we have added the nickname parameter to the url_for function . Let's see what we got. By clicking on You Profile we must go to the user’s page. Since we don’t have any links to pages of other users, you have to enter the URL manually to look at the profile of another user. For example, type to see Miguel's profile .

http://localhost:5000/user/miguel

Avatars.

Now our profile pages are rather dull. Let's add an avatar to make them more interesting.

Now we’ll write a method that will return an avatar and put it in a class ( app / models.py )

from hashlib import md5
# ...
class User(db.Model):
    # ...
    def avatar(self, size):
        return 'http://www.gravatar.com/avatar/' + md5(self.email).hexdigest() + '?d=mm&s=' + str(size)

The avatar method will return the path to the avatar, compressed to the specified size.

Gravatar will help make this very easy. You just need to create an MD5 hash from the email, and then add it to the custom URL that was above. After the hash, add other parameters to the URL . d = mm indicates that you want to return the default image when the user does not have a Gravatar account. The mm parameter returns an image with a gray silhouette of a man. The s = N parameter indicates to what size the avatar should be scaled.

Documentation for Gravatar .

Now the User class knows how to return the image, we can add it to the profile page (file app / templates / user.html):


{% extends "base.html" %}
{% block content %}

User: {{user.nickname}}


{% for post in posts %}

{{post.author.nickname}} says: {{post.body}}

{% endfor %} {% endblock %}

It is noteworthy that the User class is responsible for returning the avatar, and if at one point we decided that Gravatar is not what we want, we just rewrite the avatar method , so that it will return a different path (even those that we specify on our own server), All our templates will be presented with new avatars automatically.
We added an avatar to the top of the profile page, but at the bottom of the page we have messages next to which it would be nice to show a small avatar. For the profile page, we, of course, will show the same avatar for all messages, but then, when we transfer this functionality to the main page, we will have every message decorated with the avatar of the author of the message, and it will be really good.
To display the avatar for the post, we will make small changes to the template (file app / templates / user.html ):


{% extends "base.html" %}
{% block content %}

User: {{user.nickname}}


{% for post in posts %}
{{post.author.nickname}} says:
{{post.body}}
{% endfor %} {% endblock %}

Now our profile will look like this:
image

Reusing subpatterns (sub-template)

We have developed a profile page to display messages written by the user. Our main page also shows messages, but of any user. Now we have two types of templates that will display messages written by users. We could just copy / paste the part of the template that is responsible for displaying the message, but this is not the best idea, because when you need to make changes to the design of the message, we must remember all the templates that can display messages.

Instead, we will create a subpattern that will generate messages, then just connect the subpattern so where it is needed (file /app/templates/post.html):

{{post.author.nickname}} says:
{{post.body}}

To start, we create a message subpattern that is no different from a regular template. We take the HTML code to display the message from our template.

Then we will call the subpattern from our template using Jinja2 with the include command (file app / templates / user.html ):


{% extends "base.html" %}
{% block content %}

User: {{user.nickname}}


{% for post in posts %} {% include 'post.html' %} {% endfor %} {% endblock %}

As soon as we make the working main page, we will refer to the same subpattern until we are ready for this, therefore we will leave it for the next chapter.

More interesting profiles

Now that we have a good profile page, we don’t have enough information to display. Users like to add some information about themselves on their pages, so we’ll give them this boost and will also display it on the profile page. We will also monitor when the user last visited the site and we will also show it on the profile page.

To do our plan, we must change the database. We need to add two new fields for our User class (file app / models.py):

class User(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    nickname = db.Column(db.String(64), unique = True)
    email = db.Column(db.String(120), index = True, unique = True)
    role = db.Column(db.SmallInteger, default = ROLE_USER)
    posts = db.relationship('Post', backref = 'author', lazy = 'dynamic')
    about_me = db.Column(db.String(140))
    last_seen = db.Column(db.DateTime)

Every time we change the database, we create a new migration. Remember how, in the database part, we went through the throes of setting up a database migration system. Now we are reaping the benefits of these efforts. To add new fields to our database, just execute the script:

./db_migrate.py

And we get the answer:
New migration saved as db_repository / versions / 003_migration.py
Current database version: 3

And our two new fields are added to the database. Do not forget that if you are on Windows then the path to running the script is different.
If we do not have a migration system, you need to edit the database manually, or worse, delete it and create it again.

Now let's change the profile template, taking into account these fields (file app / templates / user.html ):


{% extends "base.html" %}
{% block content %}

User: {{user.nickname}}

{% if user.about_me %}

{{user.about_me}}

{% endif %} {% if user.last_seen %}

Last seen on: {{user.last_seen}}

{% endif %}

{% for post in posts %} {% include 'post.html' %} {% endfor %} {% endblock %}

As you can see, we use Jijna2 to show these fields, because we will only show them when there is data in them.

Now two new fields are empty for all users, so nothing will be displayed.

The Last_seen field is easy to maintain. Remember how in the previous chapter we created the before_request handler . Good place to add user login time (file app / views.py ):

from datetime import datetime
# ...
@app.before_request
def before_request():
    g.user = current_user
    if g.user.is_authenticated():
        g.user.last_seen = datetime.utcnow()
        db.session.add(g.user)
        db.session.commit()

If you enter your profile page, you will see when you last visited the site and every time you refresh the page, the time will be updated, because every time the browser makes a breforerequest request, the handler will update the time in the database.

Please note that we write the time in the standard UTC time zone . We discussed this in the previous chapter that we write all the timestamps in UTC so that they correspond to each other. There is a side effect, the time on the profile page is also displayed in UTC . We will fix this in one of the following chapters, which will focus on timestamps.

Now you need to select a place to display the “about me” field, and it would be more correct to place it in the profile editing page.

Editing a profile.

Adding a profile editing form is surprisingly easy. Let's start by creating a web form (file app / forms.py )

from flask.ext.wtf import Form
from wtforms import TextField, BooleanField, TextAreaField
from wtforms.validators import Required, Length
class EditForm(Form):
    nickname = TextField('nickname', validators = [Required()])
    about_me = TextAreaField('about_me', validators = [Length(min = 0, max = 140)])


And the template ( app / templates / edit.html file ):


{% extends "base.html" %}
{% block content %}

Edit Your Profile

{{form.hidden_tag()}}
Your nickname:{{form.nickname(size = 24)}}
About yourself:{{form.about_me(cols = 32, rows = 4)}}
{% endblock %}

Finally, we write the function handler (file app / views.py ):

from forms import LoginForm, EditForm
@app.route('/edit', methods = ['GET', 'POST'])
@login_required
def edit():
    form = EditForm()
    if form.validate_on_submit():
        g.user.nickname = form.nickname.data
        g.user.about_me = form.about_me.data
        db.session.add(g.user)
        db.session.commit()
        flash('Your changes have been saved.')
        return redirect(url_for('edit'))
    else:
        form.nickname.data = g.user.nickname
        form.about_me.data = g.user.about_me
    return render_template('edit.html',
        form = form)

Also, add a link to it from the user profile page, so that you can easily get to the editing (file app / templates / user.html ):


{% extends "base.html" %}
{% block content %}

User: {{user.nickname}}

{% if user.about_me %}

{{user.about_me}}

{% endif %} {% if user.last_seen %}

Last seen on: {{user.last_seen}}

{% endif %} {% if user.id == g.user.id %}

Edit

{% endif %}

{% for post in posts %} {% include 'post.html' %} {% endfor %} {% endblock %}

We use conditional operators to make sure that links to profile editions did not appear when you read someone else’s profile.

This is how the new screenshot of the user’s page looks, with a small description about itself.
image

Conclusion ... and homework!

We did a great job with the user profile, right?
But we have one unpleasant mistake and we must fix it.

Can you find her?

Hint. We made a mistake in the previous chapter when we did authorization. And today we wrote a new piece of code that has the same error.

Try to find it, and if you find, then feel free to write in the comments. I will explain the error and how to fix it in the next chapter.

As always, here is the link to download the application with today's changes.

I did not include the database in the archive. If you have the database of the previous chapter, just put it in the right place and run db_upgrade.py . Well, if you don’t have a previous database, create a new one using db_create.py.

Thanks for reading my tutorial.
Hope to see you in the next issue.

PS Author of the original article Miguel Grinberg

Also popular now: