Chapter 6: Profile Page and Avatars (v0.6)

This commit is contained in:
Miguel Grinberg 2017-09-14 10:48:56 -07:00
parent 854708a5f3
commit 5ce31603d8
No known key found for this signature in database
8 changed files with 140 additions and 3 deletions

View File

@ -1,6 +1,8 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms import StringField, PasswordField, BooleanField, SubmitField, \
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo TextAreaField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, \
Length
import sqlalchemy as sa import sqlalchemy as sa
from app import db from app import db
from app.models import User from app.models import User
@ -32,3 +34,9 @@ class RegistrationForm(FlaskForm):
User.email == email.data)) User.email == email.data))
if user is not None: if user is not None:
raise ValidationError('Please use a different email address.') 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')

View File

@ -1,4 +1,5 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from hashlib import md5
from typing import Optional from typing import Optional
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy.orm as so import sqlalchemy.orm as so
@ -14,6 +15,9 @@ class User(UserMixin, db.Model):
email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True,
unique=True) unique=True)
password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256)) password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256))
about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140))
last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(
default=lambda: datetime.now(timezone.utc))
posts: so.WriteOnlyMapped['Post'] = so.relationship( posts: so.WriteOnlyMapped['Post'] = so.relationship(
back_populates='author') back_populates='author')
@ -27,6 +31,10 @@ class User(UserMixin, db.Model):
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
def avatar(self, size):
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}'
@login.user_loader @login.user_loader
def load_user(id): def load_user(id):

View File

@ -1,12 +1,20 @@
from datetime import datetime, timezone
from urllib.parse import urlsplit from urllib.parse import urlsplit
from flask import render_template, flash, redirect, url_for, request from flask import render_template, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user, login_required from flask_login import login_user, logout_user, current_user, login_required
import sqlalchemy as sa import sqlalchemy as sa
from app import app, db from app import app, db
from app.forms import LoginForm, RegistrationForm from app.forms import LoginForm, RegistrationForm, EditProfileForm
from app.models import User from app.models import User
@app.before_request
def before_request():
if current_user.is_authenticated:
current_user.last_seen = datetime.now(timezone.utc)
db.session.commit()
@app.route('/') @app.route('/')
@app.route('/index') @app.route('/index')
@login_required @login_required
@ -62,3 +70,31 @@ def register():
flash('Congratulations, you are now a registered user!') flash('Congratulations, you are now a registered user!')
return redirect(url_for('login')) return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form) return render_template('register.html', title='Register', form=form)
@app.route('/user/<username>')
@login_required
def user(username):
user = db.first_or_404(sa.select(User).where(User.username == username))
posts = [
{'author': user, 'body': 'Test post #1'},
{'author': user, 'body': 'Test post #2'}
]
return render_template('user.html', user=user, posts=posts)
@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)

6
app/templates/_post.html Normal file
View File

@ -0,0 +1,6 @@
<table>
<tr valign="top">
<td><img src="{{ post.author.avatar(36) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>

View File

@ -14,6 +14,7 @@
{% if current_user.is_anonymous %} {% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a> <a href="{{ url_for('login') }}">Login</a>
{% else %} {% else %}
<a href="{{ url_for('user', username=current_user.username) }}">Profile</a>
<a href="{{ url_for('logout') }}">Logout</a> <a href="{{ url_for('logout') }}">Logout</a>
{% endif %} {% endif %}
</div> </div>

View File

@ -0,0 +1,23 @@
{% 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 %}

21
app/templates/user.html Normal file
View File

@ -0,0 +1,21 @@
{% 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 %}
{% 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 %}

View File

@ -0,0 +1,34 @@
"""new fields in user model
Revision ID: 37f06a334dbf
Revises: 780739b227a7
Create Date: 2017-09-14 10:54:13.865401
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '37f06a334dbf'
down_revision = '780739b227a7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('about_me', sa.String(length=140), nullable=True))
batch_op.add_column(sa.Column('last_seen', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('last_seen')
batch_op.drop_column('about_me')
# ### end Alembic commands ###