From 7a60858dbb9e5e1fae7e7fab820433c854f19027 Mon Sep 17 00:00:00 2001
From: Miguel Grinberg <miguel.grinberg@gmail.com>
Date: Thu, 14 Sep 2017 10:48:56 -0700
Subject: [PATCH] Chapter 6: Profile Page and Avatars (v0.6)

---
 app/forms.py                                  | 12 +++++-
 app/models.py                                 |  8 ++++
 app/routes.py                                 | 38 ++++++++++++++++++-
 app/templates/_post.html                      |  6 +++
 app/templates/base.html                       |  1 +
 app/templates/edit_profile.html               | 23 +++++++++++
 app/templates/user.html                       | 21 ++++++++++
 .../37f06a334dbf_new_fields_in_user_model.py  | 30 +++++++++++++++
 8 files changed, 136 insertions(+), 3 deletions(-)
 create mode 100644 app/templates/_post.html
 create mode 100644 app/templates/edit_profile.html
 create mode 100644 app/templates/user.html
 create mode 100644 migrations/versions/37f06a334dbf_new_fields_in_user_model.py

diff --git a/app/forms.py b/app/forms.py
index 299e943..da32ce2 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -1,6 +1,8 @@
 from flask_wtf import FlaskForm
-from wtforms import StringField, PasswordField, BooleanField, SubmitField
-from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
+from wtforms import StringField, PasswordField, BooleanField, SubmitField, \
+    TextAreaField
+from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, \
+    Length
 from app.models import User
 
 
@@ -28,3 +30,9 @@ class RegistrationForm(FlaskForm):
         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')
diff --git a/app/models.py b/app/models.py
index 5635627..7e3d48c 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,4 +1,5 @@
 from datetime import datetime
+from hashlib import md5
 from app import db, login
 from flask_login import UserMixin
 from werkzeug.security import generate_password_hash, check_password_hash
@@ -10,6 +11,8 @@ class User(UserMixin, db.Model):
     email = db.Column(db.String(120), index=True, unique=True)
     password_hash = db.Column(db.String(128))
     posts = db.relationship('Post', backref='author', lazy='dynamic')
+    about_me = db.Column(db.String(140))
+    last_seen = db.Column(db.DateTime, default=datetime.utcnow)
 
     def __repr__(self):
         return '<User {}>'.format(self.username)
@@ -20,6 +23,11 @@ class User(UserMixin, db.Model):
     def check_password(self, password):
         return check_password_hash(self.password_hash, password)
 
+    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)
+
 
 @login.user_loader
 def load_user(id):
diff --git a/app/routes.py b/app/routes.py
index 96689df..76a502f 100644
--- a/app/routes.py
+++ b/app/routes.py
@@ -1,11 +1,19 @@
+from datetime import datetime
 from flask import render_template, flash, redirect, url_for, request
 from flask_login import login_user, logout_user, current_user, login_required
 from werkzeug.urls import url_parse
 from app import app, db
-from app.forms import LoginForm, RegistrationForm
+from app.forms import LoginForm, RegistrationForm, EditProfileForm
 from app.models import User
 
 
+@app.before_request
+def before_request():
+    if current_user.is_authenticated:
+        current_user.last_seen = datetime.utcnow()
+        db.session.commit()
+
+
 @app.route('/')
 @app.route('/index')
 @login_required
@@ -60,3 +68,31 @@ def register():
         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)
+
+
+@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)
diff --git a/app/templates/_post.html b/app/templates/_post.html
new file mode 100644
index 0000000..d020426
--- /dev/null
+++ b/app/templates/_post.html
@@ -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>
diff --git a/app/templates/base.html b/app/templates/base.html
index 775a3ce..0c81af2 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -13,6 +13,7 @@
             {% 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>
diff --git a/app/templates/edit_profile.html b/app/templates/edit_profile.html
new file mode 100644
index 0000000..e2471ac
--- /dev/null
+++ b/app/templates/edit_profile.html
@@ -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 %}
diff --git a/app/templates/user.html b/app/templates/user.html
new file mode 100644
index 0000000..5bacb4e
--- /dev/null
+++ b/app/templates/user.html
@@ -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 %}
diff --git a/migrations/versions/37f06a334dbf_new_fields_in_user_model.py b/migrations/versions/37f06a334dbf_new_fields_in_user_model.py
new file mode 100644
index 0000000..2b006d4
--- /dev/null
+++ b/migrations/versions/37f06a334dbf_new_fields_in_user_model.py
@@ -0,0 +1,30 @@
+"""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! ###
+    op.add_column('user', sa.Column('about_me', sa.String(length=140), nullable=True))
+    op.add_column('user', sa.Column('last_seen', sa.DateTime(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('user', 'last_seen')
+    op.drop_column('user', 'about_me')
+    # ### end Alembic commands ###