📜 ⬆️ ⬇️

Flask Mega-Tutorial, Part 6: Profile page and avatars (edition 2018)

blog.miguelgrinberg.com


Miguel grinberg




<<< previous next >>>


This article is a translation of the sixth part of the new edition of Miguel Greenberg’s textbook, the issue of which the author plans to complete in May 2018. The previous translation has long since lost its relevance.


I, for my part, will try to keep up with the translation.




This is the sixth edition of the Flask Mega-Tutorial series, in which I will tell you how to create a user profile page.


For reference, below is a list of articles in this series.



Note 1: If you are looking for old versions of this course, this is here .


Note 2: If suddenly you would like to speak in support of my (Miguel) work on this blog, or simply do not have the patience to wait for a week of the article, I (Miguel Greenberg) offer a full version of this guide a packed e-book or video. For more information, visit learn.miguelgrinberg.com .


This chapter will focus on adding user profile pages to an application. A user profile page is a page that provides information about the user, usually entered by the users themselves. I'll show you how to create profile pages for all users dynamically, and then I will add a small profile editor that users can use to enter their information.


GitHub links for this chapter: Browse , Zip , Diff .


User profile page


To create a user profile page, let's first write a new browse function that will be displayed in the / user / <user name> URL.


@app.route('/user/<username>') @login_required def user(username): user = User.query.filter_by(username=username).first_or_404() posts = [ {'author': user, 'body': 'Test post #1'}, {'author': user, 'body': 'Test post #2'} ] return render_template('user.html', user=user, posts=posts) 

The decorator @app.route , which I used to declare this viewing function, is slightly different from the previous ones. In this case, I have a dynamic component, which is designated as a component of the URL <username> , which is enclosed in triangular brackets, < and > . When a route has a dynamic component, Flask accepts any text in this part of the URL and calls the view function with the actual text as an argument. For example, if the client browser requests the URL: / user / susan , the view function will be called with the username as the argument set to «susan» . This browsing feature will be available only to registered users, so I added the @login_required handler from Flask-Login.


The implementation of this viewing function is quite simple. First, I try to load a user from the database using a query by username. You have already seen that a database query can be executed by calling all() , if you want to get all the results, or first() , if you want to get only the first result, or if there is nothing that meets the search criteria, then these methods will return None. In this view function, I use the first() variant, called first_or_404(), which works exactly like first() when there are results, but if there are no results, a 404 error is automatically sent back to the client. By executing the query in this way, I cannot check whether the query returned to the user, because when the username does not exist in the database, the function will not return, and the exception 404 will be raised instead.


If the database query does not cause a 404 error , this means that a user with the specified user name was found. Then I initialize the temporary list of messages for this user, finally, I create a new template user.html , to which I pass the user object and the list of messages.


The user.html template is shown below:


 {% extends "base.html" %} {% block content %} <h1>User: {{ user.username }}</h1> <hr> {% for post in posts %} <p> {{ post.author.username }} says: <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %} 

The profile page is complete, but the link to it does not exist anywhere on the website. To make it easier for users to check their own profile, I will add a link to it in the navigation bar above:


 <div> Microblog: <a href="{{ url_for('index') }}">Home</a> {% if current_user.is_anonymous %} <a href="{{ url_for('login') }}">Login</a> {% else %} <a href="{{ url_for('user', username=current_user.username) }}">Profile</a> <a href="{{ url_for('logout') }}">Logout</a> {% endif %} </div> 

The only interesting change here is the url_for() call, which is used to create a link to the profile page. Because the user profile viewer function accepts a dynamic argument, the url_for() function takes a value for it as a keyword argument. Since this is a link that points to a user profile in a log, I can use current_user Flask-Login to generate the correct URL.



Try it! Clicking the "Profile" link at the top of the page will take you to your user page. At this stage there are no links that will be displayed on the profile page of other users, but if you want to access these pages, you can enter the URL manually in the address bar of the browser. For example, if you have a user named “john” registered in your application, you can view the corresponding user profile by typing http: // localhost: 5000 / user / john in the address bar.


Avatars


I’m sure you will agree that the profile pages I’ve just built are pretty boring. To make them more interesting, I'm going to add custom avatars, but instead of dealing with a possibly large collection of downloaded images on the server, I'm going to use the Gravatar service to provide images for all users.


Gravatar service is very easy to use. To request an image for this user, the URL is https://www.gravatar.com/avatar/ , where <hash> is the MD5 hash of the user's email address. Below you can see how to get the Gravatar URL for a user with an email address john@example.com :

 >>> from hashlib import md5 >>> 'https://www.gravatar.com/avatar/' + md5(b'john@example.com').hexdigest() 'https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6' 

If you want to see an actual example, you can use my own Gravatar URL - https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35 (' https://www.gravatar.com/avatar/4f3699b436c12996ae54771200f21888 '). Here is what Gravatar returns for this URL:



By default, the returned image size is 80x80 pixels, but another size can be requested by adding the s argument to the URL request string. For example, to get my own avatar as a 128x128 image, the URL is: https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35?s=128 .


Another interesting argument that Gravatar can supply as an argument to a query string is d , which determines which image Gravatar provides to users who do not have an avatar registered with the service. My favorite is called “id”, which returns a nice geometric design that is different for each letter. For example:



Please note that some web browser extensions, such as Ghostery, block Gravatar images, as they believe that Automattic (owners of Gravatar) can determine which sites you visit based on the requests they receive for your avatar. If you don’t see avatars in your browser, the problem is most likely in browser extensions.


Since avatars are associated with users, it makes sense to add logic that generates avatar URLs for the user model.


 from hashlib import md5 # ... class User(UserMixin, db.Model): # ... def avatar(self, size): digest = md5(self.email.lower().encode('utf-8')).hexdigest() return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format( digest, size) 

The new method avatar() class User returns the URL of the user's avatar image, scaled to the required size in pixels. For users who do not have a registered avatar, an “id” image will be created. To generate the MD5 hash, I convert the email address to lower case, as this is required by Gravatar. Then, I convert the resulting hash object to a hexadecimal string (.hexdigest () method) before passing it to the hash function.


If you are interested in exploring other options offered by the Gravatar service, visit their documentation site .


The next step is to insert the avatar images into the user profile template:


 {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{ user.avatar(128) }}"></td> <td><h1>User: {{ user.username }}</h1></td> </tr> </table> <hr> {% for post in posts %} <p> {{ post.author.username }} says: <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %} 

The big plus is that the custom class is responsible for returning the URLs of avatars. And if on some day I decide that the Gravatar avatars are not what I want, I can simply rewrite the avatar() method to return different URLs -address, and all templates will start showing new avatars automatically.


I have a nice big avatar at the top of the user profile page, but in fact there is no reason to stop there. I have a few posts from the user below, in which everyone can have a small avatar. Of course, for the user profile page, all messages will have the same avatar, but then I can implement the same functionality on the main page, and then each post will be decorated with the author's avatar, and it will look very beautiful.


To show avatars for individual posts, I just need to make another small change in the template:


 {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{ user.avatar(128) }}"></td> <td><h1>User: {{ user.username }}</h1></td> </tr> </table> <hr> {% for post in posts %} <table> <tr valign="top"> <td><img src="{{ post.author.avatar(36) }}"></td> <td>{{ post.author.username }} says:<br>{{ post.body }}</td> </tr> </table> {% endfor %} {% endblock %} 

That’s Susan’s



(Approx. Translator) And so with a real Gravatar account.



Using sub-templates Jinja2


I designed a user profile page to display posts written by the user, along with my avatars. Now I want the index page to also display messages with a similar location. I could just zakopipasti part of the template, which concerns the rendering of the message, but this is not correct, because later, if I decide to make changes to this layout, I have to remember to update both templates.


Instead, I'm going to make a subpattern that just displays one message, and then I will refer to it from both the user.html templates and index.html . First, I can create a subpattern with HTML markup for a single post. I will call, perhaps, this sample app / templates / _post.html . The `_ 'prefix is ​​simply a naming convention that helps recognize which template files are subtemplates.


 <table> <tr valign="top"> <td><img src="{{ post.author.avatar(36) }}"></td> <td>{{ post.author.username }} says:<br>{{ post.body }}</td> </tr> </table> 

To call this subpattern from the user.html template, I use the include Jinja2 statement:


 {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{ user.avatar(128) }}"></td> <td><h1>User: {{ user.username }}</h1></td> </tr> </table> <hr> {% for post in posts %} {% include '_post.html' %} {% endfor %} {% endblock %} 

The index page of the application is actually not yet formed, so I’m not going to add this functionality yet.


More interesting profiles


One of the problems new user profile pages face is that they don’t really show much. Users love to talk about something on these pages, so I will let them write something about themselves to show here.
I will also keep track of when the last time each user accessed the site, as well as display it on your profile page.


The first thing I need to do to support all this additional information is to expand the user table in the database with two new fields:


 class User(UserMixin, db.Model): # ... about_me = db.Column(db.String(140)) last_seen = db.Column(db.DateTime, default=datetime.utcnow) 

Every time the database changes, you need to create a database migration. In Chapter 4, I showed you how to set up an application to track database changes using migration scripts. Now I have two new fields that I want to add to the database, so the first step is to create a migration script:




Note translator : I advise the team, and typing the texts yourself. Get errors / typos. Correct them. All this is a learning process that will greatly help in the future.


Here is an example error:


 (venv) C:\microblog>flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. (venv) C:\microblog>flask db mograte -m "new fields in user model" Usage: flask db [OPTIONS] COMMAND [ARGS]... Error: No such command "mograte". 

continue ...




 (venv) C:\microblog>flask db migrate -m "new fields in user model" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added column 'user.about_me' INFO [alembic.autogenerate.compare] Detected added column 'user.last_seen' Generating C:\microblog\migrations\versions\45833c85abc8_new_fields_in_user_model.py ... done (venv) C:\microblog> 

The result of the migrate command looks good, as it shows that two new fields were found in the User class. Now I can apply this change to the database:


 (venv) C:\microblog>flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade f8c5670875a3 -> 45833c85abc8, new fields in user model 

I hope you understand how useful it is to work with the migration infrastructure. Any users who were in the database still exist; the migration structure quickly applies changes in the migration scenario without destroying any data.


In the next step, I'm going to add these two new fields to the user profile template:


 {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{ user.avatar(128) }}"></td> <td> <h1>User: {{ user.username }}</h1> {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} {% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %} </td> </tr> </table> ... {% endblock %} 

Notice that I wrap these two fields in Jinja2 conditional expressions, because I want them to be visible if they are filled. At this stage, these two new fields are empty for all users, so you will not see these fields if you start the application right now.


Record of last visit time for user


Let's start with the last_seen field, the simpler of the two. What I want to do is to record the current time in this field for a particular user whenever the user sends a request to the server.
Adding a login to set this field to various browsing functions that can be requested from the browser is obviously impractical, but executing some common logic before a request sent to the browsing function is a common task in web applications that Flask offers it as a native function . Take a look at the solution:


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

Flask's @before_request decorator registers a decorated function that must be executed immediately before the view function. This is very useful, because now I can insert the code that I want to execute before any view function in the application, and I can use it in one place. The implementation simply checks to see if current_user registered, in which case it sets the last field to the current time. I already mentioned this, the server application should work in uniform time units, and the standard practice is to use the UTC time zone. Using the local time of the system is not a good idea, because what happens in the database depends on your location.
The final step is to commit the database session, so that the change made above is written to the database. If you are wondering why there is no db.session.add() before committing, consider that when you refer to current_user , Flask-Login will call the callback function of the user’s loader that will launch a database request that will put the target user in the database session. data. So you can add the user to this function again, but this is not necessary because it already exists.


If you view your profile page after making this change, you will see the "Last seen on" line with a time close to the current one.
And if you go from the profile page and then return, you will see that the time is constantly updated.


The fact that I store these time stamps in the UTC time zone makes the time displayed on the profile page also in UTC. To a heap of all this, the time format is not what you expect, since we see the display of the internal representation of the Python datetime object. At the moment I will not worry about these two problems, as I will talk about the topic of handling dates and times in the web application in the next chapter.



Profile editor


For good users, you need to provide a form in which they can enter some information about themselves. The form will allow users to change their username and other data, as well as write something about themselves in order to be stored in the new about_me field. Let's write a class for this form:


 from wtforms import StringField, TextAreaField, SubmitField from wtforms.validators import DataRequired, Length # ... class EditProfileForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) submit = SubmitField('Submit') 

I am using a new field type and a new validator in this form. For the "About" field, I use TextAreaField , which is a multi-line field in which the user can enter text. To check this field, I use Length , which will ensure that the entered text is between 0 and 140 characters, which is the space that I have allocated for the corresponding field in the database.


The template that displays this form is shown below:


app / templates / edit_profile.html

 {% extends "base.html" %} {% block content %} <h1>Edit Profile</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.username.label }}<br> {{ form.username(size=32) }}<br> {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.about_me.label }}<br> {{ form.about_me(cols=50, rows=4) }}<br> {% for error in form.about_me.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %} 

And finally, here is the function that links everything together:


 from app.forms import EditProfileForm @app.route('/edit_profile', methods=['GET', 'POST']) @login_required def edit_profile(): form = EditProfileForm() if form.validate_on_submit(): current_user.username = form.username.data current_user.about_me = form.about_me.data db.session.commit() flash('Your changes have been saved.') return redirect(url_for('edit_profile')) elif request.method == 'GET': form.username.data = current_user.username form.about_me.data = current_user.about_me return render_template('edit_profile.html', title='Edit Profile', form=form) 

This view function is slightly different from the other form processing. If validate_on_submit() returns True , I copy the data from the form into a user object, and then write the object to the database. But when validate_on_submit() returns False , it can be caused by two different reasons. Firstly, this may be due to the fact that the browser simply sent a GET request to which I need to respond by providing the original version of the form template. Secondly, this is also possible when the browser sends a POST request with form data, but something in this data is invalid. For this form, I need to consider these two cases separately. When a form is requested for the first time with a GET request, I want to pre-fill the fields with data stored in the database, so I need to do the opposite of what I did in the case of sending, and move the data stored in the user fields to the form, because this ensures that these form fields have current data stored for the user. But in case of a validation error, I do not want to write anything in the form fields, because they have already been filled with WTForms . To distinguish between these two cases, I check request.method , which will be GET for the initial request, and POST for sending, which did not pass the test.



To allow users to access the profile editor page, add a link to the profile page:


  {% if user == current_user %} <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p> {% endif %} 

Notice the tricky conditional code I use to make sure that the “Edit” link appears when you view your own profile, but not when someone is viewing yours. Or you are someone else's profile.



<<< previous next >>>


')

Source: https://habr.com/ru/post/346348/


All Articles