☆☆ 新着記事 ☆☆

2019年3月7日木曜日

Flask(4) ユーザプロファイルページ (動的URL)作成



このPost (User Profile) のOutput Image


* Followers/Followingは、次のポストで解説。




1. ユーザプロファイル・ページを作成

ユーザ属性をURLに埋め込んだ、ログインしたユーザ用のページを作成する。
最終的なURLイメージ。 /user/<username> URL.

View function(routes.py)を変更する。

app/routes.py: User profile view function
@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)

<username> :動的なURLを作るときに<>を使用する。(usernameは、forms.pyで定義されたフィールド名で、htmlのpost methodで値を受け取る。models.pyで、同じ’username’でdatabaseに格納されている。)
usernameは、queryでデータベースから抽出される。
first_or_404():fileterであるfirst()の一種で、値があれば呼び出し、無ければ404をclientに返す。404であれば、データベースにマッチするユーザが存在しないことが分かり便利。


次にレンダーする、user.htmlを作成する。

Next I initialize a fake list of posts for this user, finally render a new user.html template to which I pass the user object and the list of posts.
The user.html template is shown below:

app/templates/user.html: User profile template
{% 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 %}

ナビゲーション・バーにuser.htmlへのリンクを追加。

app/templates/base.html: User profile template
    <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>

url_for('user',username=current_user.username)
ログインしたユーザにアクセスを許可するページなので、usernameは、current_userから取り出すことができる。

At this point there are no links that will take to the profile page of other users, but if you want to access those pages you can type the URL by hand i



2.Postをユーザ・プロファイル(user.htm)とindexページに表示する
(Jinja2のSub-Templates機能を利用する。)

index.html、又は、user.htmlの何れか一方をコピーして、もう一方に張り付けても良いが、それだと将来、レイアウト変更の必要性が出た場合、2つのテンプレートを変更しなくてはいけない。 そこで、
1つのPostだけを扱うサブ・テンプレートを作成し、これを両方のテンプレートから参照することにする。

最初に、
app/templates/_post.html.
を作る。 _ プレフィックスは、サブフォルダを作るときの命名。


app/templates/_post.html: Post sub-template
    <table>
        <tr valign="top">
          <!--画像用:別途設定
            <td><img src="{{ post.author.avatar(36) }}"></td>
          -->
            <td>{{ post.author.username }} says:<br>{{ post.body }}</td>
        </tr>
    </table>


この記述をuser.htmlから呼び出すために、以下の記述をする。
(全面的に前回までのものを書き換え)

app/templates/user.html: User avatars in posts
{% 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 %}

index.pageも変更する必要があるが、後でまとめて変更する。

3. ユーザ・プロファイル(user.html)に表示する情報を追加する。

ユーザが記述できる‘about_me’と最後にログインした時間を表示する’last_seen’をユーザ・プロファイル(user.html)に表示する

models.pyを修正し、DBの行を追加する。

app/models.py: New fields in user model
class User(UserMixin, db.Model):
    # ...
    about_me = db.Column(db.String(140))
    last_seen = db.Column(db.DateTime, default=datetime.utcnow)

リレーショナル・データベースの構成を変更したので、Flask-Migrationの手順に従って、
データベースのアップデートをする。

(venv) $ 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 /home/miguel/microblog/migrations/versions/37f06a334dbf_new_fields_in_user_model.py ... done

"new fields in user model" は、作成されるスクリプトのファイル名に付加されるテキスト。任意で良い。

(venv) $ 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 780739b227a7 -> 37f06a334dbf, new fields in user model

追加された2つのフィールドをユーザ・プロファイルに追加する。

app/templates/user.html: Show user information in 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 %}

{% if user.about_me %}は、新しく追加したフィールドに内容が記入された時にのみ
表示させる為のif文。

4. 訪問時間を記録する。

Adding the login to set this field on every possible view function that can be requested from the browser is obviously impractical, but executing a bit of generic logic ahead of a request being dispatched to a view function is such a common task in web applications that Flask offers it as a native feature.

app/routes.py: Record time of last visit
from datetime import datetime

@app.before_request  <- デコレータ
def before_request():
    if current_user.is_authenticated:
        current_user.last_seen = datetime.utcnow()
        db.session.commit()

The @before_request decorator from Flask register the decorated function to be executed right before the view function. This is extremely useful because now I can insert code that I want to execute before any view function in the application, and I can have it in a single place. The implementation simply checks if the current_user is logged in, and in that case sets the last_seen field to the current time. I mentioned this before, a server application needs to work in consistent 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 then what goes in the database is dependent on your location. The last 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 the commit, consider that when you reference current_user, Flask-Login will invoke the user loader callback function, which will run a database query that will put the target user in the database session. So you can add the user again in this function, but it is not necessary because it is already there.

(時間の表示のさせ方については別途)

5. プロファイルの修正

forms.pyに、新たに更新用のclassを作成する。

app/forms.py: Profile editor 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')

このformを修正するテンプレートを新規に作成する。

app/templates/edit_profile.html: Profile editor form

{% 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 view function that ties everything together:

app/routes.py: Edit profile view function
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 to the other ones that process a form. If validate_on_submit() returns True I copy the data from the form into the user object and then write the object to the database. But when validate_on_submit() returns False it can be due to two different reasons. First, it can be because the browser just sent a GET request, which I need to respond by providing an initial version of the form template. It can also be when the browser sends a POST request with form data, but something in that data is invalid. For this form, I need to treat these two cases separately. When the form is being requested for the first time with a GET request, I want to pre-populate the fields with the data that is stored in the database, so I need to do the reverse of what I did on the submission case and move the data stored in the user fields to the form, as this will ensure that those form fields have the current data stored for the user. But in the case of a validation error I do not want to write anything to the form fields, because those were already populated by WTForms. To distinguish between these two cases, I check request.method, which will be GET for the initial request, andPOST for a submission that failed validation.

To make it easy for users to access the profile editor page, I can add a link in their profile page:
app/templates/user.html: Edit profile link
                {% if user == current_user %}
                <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
                {% endif %}
Pay attention to the clever conditional I'm using to make sure that the Edit link appears when you are viewing your own profile, but not when you are viewing the profile of someone else.

=======今回ファイルの最終形をメモ
05 forms
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User


class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')


class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('Please use a different username.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')

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

05 routes.py
from flask import render_template, flash, redirect, url_for
from app import app
from app.forms import LoginForm
from flask_login import current_user, login_user # Log-inしたユーザ処理用
from app.models import User # Log-inしたユーザ処理用
from flask_login import logout_user # ユーザのログアウト処理用
from flask import request # redirect back用
from werkzeug.urls import url_parse # redirect back用
from app import db  #ユーザレジとレーション用
from app.forms import RegistrationForm #ユーザレジとレーション用
from app.forms import EditProfileForm

@app.route('/')
@app.route('/index')
def index():
    return render_template('index.html', title='Home', posts=posts)


@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
   return redirect(url_for('index'))

    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
       if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
# redirect back用の設定
             next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        return redirect(next_page)


    return render_template('login.html', title='Sign In', form=form)

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)

@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)

# Profile修正用
@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)


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

05 base.html
<html>
    <head>
        {% if title %}
        <title>{{ title }} - Microblog</title>
        {% else %}
        <title>Welcome to Microblog</title>
        {% endif %}
    </head>
    <body>
    <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>
        <hr>
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <ul>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </body>
</html>

05 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 %}
05 user.html.

{% extends "base.html" %}

{% block content %}
    <table>
        <tr valign="top">
            <!--画像用:別途設定
<td><img src="{{ user.avatar(128) }}"></td>-->
<td>
<h1>User: {{ user.username }}</h1>
<!--DBにfieldを追加した後に、追加する記述 - - >
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
        {% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %}
<!—プロファイル修正の為のLinkの記述 - - >
              {% if user == current_user %}
        <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
        {% endif %}
</td>
        </tr>
    </table>
    <hr>
    {% for post in posts %}
        {% include '_post.html' %}
    {% endfor %}
{% endblock %}

0 件のコメント:

コメントを投稿