From 75aacd279e6988cc8bc14cab7a2d643d40d1445c Mon Sep 17 00:00:00 2001
From: kaiyou <pierre@jaury.eu>
Date: Sat, 14 Sep 2019 14:26:51 +0200
Subject: [PATCH] Add an event history and display it on the home page

---
 .../{3fe66efb6088_.py => b2fe21a1da94_.py}    | 40 +++++++++++-----
 trurt/account/profiles.py                     | 11 +++--
 trurt/account/settings.py                     |  3 +-
 trurt/account/templates/account_home.html     | 28 +++++++++++
 trurt/account/templates/account_pick.html     | 14 ++++++
 trurt/configuration.py                        |  3 +-
 trurt/manage.py                               | 13 +++++
 trurt/models.py                               | 47 +++++++++++++++++--
 trurt/templates/base.html                     |  1 +
 trurt/templates/macros.html                   | 36 ++++++++++++++
 10 files changed, 174 insertions(+), 22 deletions(-)
 rename migrations/versions/{3fe66efb6088_.py => b2fe21a1da94_.py} (59%)
 create mode 100644 trurt/account/templates/account_pick.html
 create mode 100644 trurt/templates/macros.html

diff --git a/migrations/versions/3fe66efb6088_.py b/migrations/versions/b2fe21a1da94_.py
similarity index 59%
rename from migrations/versions/3fe66efb6088_.py
rename to migrations/versions/b2fe21a1da94_.py
index 0a2fc98..91a95e5 100644
--- a/migrations/versions/3fe66efb6088_.py
+++ b/migrations/versions/b2fe21a1da94_.py
@@ -1,14 +1,14 @@
 """ empty message
 
-Revision ID: 3fe66efb6088
+Revision ID: b2fe21a1da94
 Revises:
-Create Date: 2019-09-14 12:13:37.847931
+Create Date: 2019-09-14 13:27:34.971323
 """
 from alembic import op
 import sqlalchemy as sa
 
 
-revision = '3fe66efb6088'
+revision = 'b2fe21a1da94'
 down_revision = None
 branch_labels = None
 depends_on = None
@@ -23,16 +23,16 @@ def upgrade():
     sa.Column('max_profiles', sa.Integer(), nullable=True),
     sa.Column('config', sa.String(), nullable=True),
     sa.Column('uuid', sa.String(length=36), nullable=False),
-    sa.Column('created_at', sa.Date(), nullable=False),
-    sa.Column('updated_at', sa.Date(), nullable=True),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.Column('updated_at', sa.DateTime(), nullable=True),
     sa.Column('comment', sa.String(length=255), nullable=True),
     sa.PrimaryKeyConstraint('uuid')
     )
     op.create_table('user',
     sa.Column('username', sa.String(length=255), nullable=False),
     sa.Column('uuid', sa.String(length=36), nullable=False),
-    sa.Column('created_at', sa.Date(), nullable=False),
-    sa.Column('updated_at', sa.Date(), nullable=True),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.Column('updated_at', sa.DateTime(), nullable=True),
     sa.Column('comment', sa.String(length=255), nullable=True),
     sa.PrimaryKeyConstraint('uuid'),
     sa.UniqueConstraint('username')
@@ -42,8 +42,8 @@ def upgrade():
     sa.Column('value', sa.String(), nullable=True),
     sa.Column('extra', sa.String(), nullable=True),
     sa.Column('uuid', sa.String(length=36), nullable=False),
-    sa.Column('created_at', sa.Date(), nullable=False),
-    sa.Column('updated_at', sa.Date(), nullable=True),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.Column('updated_at', sa.DateTime(), nullable=True),
     sa.Column('comment', sa.String(length=255), nullable=True),
     sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], ),
     sa.PrimaryKeyConstraint('uuid')
@@ -54,18 +54,36 @@ def upgrade():
     sa.Column('username', sa.String(length=255), nullable=False),
     sa.Column('status', sa.String(length=25), nullable=False),
     sa.Column('uuid', sa.String(length=36), nullable=False),
-    sa.Column('created_at', sa.Date(), nullable=False),
-    sa.Column('updated_at', sa.Date(), nullable=True),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.Column('updated_at', sa.DateTime(), nullable=True),
     sa.Column('comment', sa.String(length=255), nullable=True),
     sa.ForeignKeyConstraint(['service_uuid'], ['service.uuid'], ),
     sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], ),
     sa.PrimaryKeyConstraint('uuid')
     )
+    op.create_table('history',
+    sa.Column('user_uuid', sa.String(length=36), nullable=True),
+    sa.Column('profile_uuid', sa.String(length=36), nullable=True),
+    sa.Column('service_uuid', sa.String(length=36), nullable=True),
+    sa.Column('actor_uuid', sa.String(length=36), nullable=True),
+    sa.Column('category', sa.String(length=25), nullable=True),
+    sa.Column('value', sa.String(), nullable=True),
+    sa.Column('uuid', sa.String(length=36), nullable=False),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.Column('updated_at', sa.DateTime(), nullable=True),
+    sa.Column('comment', sa.String(length=255), nullable=True),
+    sa.ForeignKeyConstraint(['actor_uuid'], ['user.uuid'], ),
+    sa.ForeignKeyConstraint(['profile_uuid'], ['profile.uuid'], ),
+    sa.ForeignKeyConstraint(['service_uuid'], ['service.uuid'], ),
+    sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], ),
+    sa.PrimaryKeyConstraint('uuid')
+    )
     # ### end Alembic commands ###
 
 
 def downgrade():
     # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('history')
     op.drop_table('profile')
     op.drop_table('auth')
     op.drop_table('user')
diff --git a/trurt/account/profiles.py b/trurt/account/profiles.py
index 6eb8f0a..2bfbbe3 100644
--- a/trurt/account/profiles.py
+++ b/trurt/account/profiles.py
@@ -1,5 +1,6 @@
 from trurt.account import blueprint
 from trurt.sso import forms as sso_forms
+from trurt import models, utils
 
 import flask_login
 import flask
@@ -13,13 +14,13 @@ def profiles():
 @blueprint.route("/pick")
 @flask_login.login_required
 def pick():
-    service_spn = flask.request.args.get("service_spn") or flask.abort(404)
-    service = models.Service.query.filter_by(spn=service_spn).first_or_404()
+    service_uuid = flask.request.args.get("service_uuid") or flask.abort(404)
+    service = models.Service.query.get(service_uuid) or flask.arort(404)
     profiles = models.Profile.query.filter_by(
-        service_id=service.id,
-        user_id=flask_login.current_user.id
+        service_uuid=service.uuid,
+        user_uuid=flask_login.current_user.uuid
     )
     form = sso_forms.SSOValidateForm()
-    return flask.render_template("sso_pick.html",
+    return flask.render_template("account_pick.html",
         service=service, profiles=profiles, form=form,
         action=utils.url_or_intent("account.status"))
diff --git a/trurt/account/settings.py b/trurt/account/settings.py
index a16c1e8..7beae73 100644
--- a/trurt/account/settings.py
+++ b/trurt/account/settings.py
@@ -7,7 +7,8 @@ import flask
 @blueprint.route("/home")
 @flask_login.login_required
 def home():
-    return flask.render_template("account_home.html")
+    history = flask_login.current_user.history
+    return flask.render_template("account_home.html", history=history)
 
 
 @blueprint.route("/password")
diff --git a/trurt/account/templates/account_home.html b/trurt/account/templates/account_home.html
index 70c9b96..e1a375d 100644
--- a/trurt/account/templates/account_home.html
+++ b/trurt/account/templates/account_home.html
@@ -2,3 +2,31 @@
 
 {% block title %}My account{% endblock %}
 {% block subtitle %}overview of {{ current_user.username }}{% endblock %}
+
+{% block content %}
+<div class="row">
+  <div class="col-md-6 col-s-12">
+    <section class="content">
+      <div class="row">
+        <div class="col-md-6 col-xs-12">
+          {{ macros.infobox("Account age", "{} days".format((current_user.created_at.today() - current_user.created_at).total_seconds() // 86400), "aqua", "calendar") }}
+        </div>
+        <div class="col-md-6 col-xs-12">
+          {{ macros.infobox("Profile count", current_user.profiles.__len__(), "red", "users") }}
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-md-6 col-xs-12">
+          {{ macros.infobox("Pending requests", "0", "green", "hourglass") }}
+        </div>
+        <div class="col-md-6 col-xs-12">
+          {{ macros.infobox("Role", "registered user", "yellow", "lock") }}
+        </div>
+      </div>
+    </section>
+  </div>
+  <div class="col-md-6 col-s-12">
+    {{ macros.timeline(history) }}
+  </div>
+</div>
+{% endblock %}
diff --git a/trurt/account/templates/account_pick.html b/trurt/account/templates/account_pick.html
new file mode 100644
index 0000000..4449f54
--- /dev/null
+++ b/trurt/account/templates/account_pick.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+
+{% block title %}Pick a profile{% endblock %}
+{% block subtitle %}for the service {{ service.name }}{% endblock %}
+
+{% block content %}
+{% for profile in profiles %}
+<form method="POST" action="{{ action }}">
+    {{ form.hidden_tag() }}
+    <input type="hidden" name="profile_uuid" value="{{ profile.uuid }}">
+    <input type="submit" value="{{ profile.username }}">
+</form>
+{% endfor %}
+{% endblock %}
diff --git a/trurt/configuration.py b/trurt/configuration.py
index d2a1688..6a3e8b1 100644
--- a/trurt/configuration.py
+++ b/trurt/configuration.py
@@ -9,7 +9,8 @@ DEFAULT_CONFIG = {
     'SQLALCHEMY_DATABASE_URI': 'sqlite:////tmp/trurt.db',
     'SQLALCHEMY_TRACK_MODIFICATIONS': False,
     'SECRET_KEY': 'changeMe',
-    'TEMPLATES_AUTO_RELOAD': False
+    'TEMPLATES_AUTO_RELOAD': False,
+    'MAIL_DOMAIN': 'tedomum.net'
 }
 
 class ConfigManager(dict):
diff --git a/trurt/manage.py b/trurt/manage.py
index 9c550eb..f88e06c 100644
--- a/trurt/manage.py
+++ b/trurt/manage.py
@@ -23,6 +23,11 @@ def create_user(username, password):
     auth = models.Auth()
     auth.set_password(password)
     user.auths.append(auth)
+    event = models.History()
+    event.user = user
+    event.category = models.History.REGISTER
+    event.comment = "Created from the command line"
+    models.db.session.add(event)
     models.db.session.add(user)
     models.db.session.add(auth)
     models.db.session.commit()
@@ -40,5 +45,13 @@ def create_profile(username, service_uuid, profile_username):
     profile.user = user
     profile.service = service
     profile.username = profile_username
+    event = models.History()
+    event.user = user
+    event.service = service
+    event.profile = profile
+    event.category = models.History.CREATE
+    event.value = profile.username
+    event.comment = "Created from the command line"
+    models.db.session.add(event)
     models.db.session.add(profile)
     models.db.session.commit()
diff --git a/trurt/models.py b/trurt/models.py
index 31b6868..4dc8553 100644
--- a/trurt/models.py
+++ b/trurt/models.py
@@ -1,7 +1,7 @@
 from passlib import context, hash
 from flask import current_app as app
 from sqlalchemy.ext import declarative
-from datetime import date
+from datetime import datetime
 
 import flask_sqlalchemy
 import sqlalchemy
@@ -26,11 +26,11 @@ class Base(flask_sqlalchemy.Model):
 
     @declarative.declared_attr
     def created_at(cls):
-        return sqlalchemy.Column(sqlalchemy.Date, nullable=False, default=date.today)
+        return sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.now)
 
     @declarative.declared_attr
     def updated_at(cls):
-        return sqlalchemy.Column(sqlalchemy.Date, nullable=True, onupdate=date.today)
+        return sqlalchemy.Column(sqlalchemy.DateTime, nullable=True, onupdate=datetime.today)
 
     @declarative.declared_attr
     def comment(cls):
@@ -132,6 +132,7 @@ class Profile(db.Model):
     __tablename__ = "profile"
 
     ACTIVE = "active"
+    DELETED = "deleted"
 
     user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
     service_uuid = db.Column(db.String(36), db.ForeignKey(Service.uuid))
@@ -145,4 +146,42 @@ class Profile(db.Model):
 
     @property
     def email(self):
-        return self.uuid + "@kaiyou.fr"
+        return "{}@{}".format(self.uuid, app.config.get("MAIL_DOMAIN"))
+
+
+class History(db.Model):
+    """ Records an even in an account's or profile's lifetime.
+    """
+    __tablename__ = "history"
+
+    REGISTER = "register"
+    CREATE = "create"
+    DELETE = "delete"
+    NOTE = "note"
+    STATUS = "status"
+
+    DESCRIPTION = {
+        REGISTER: "registered this account",
+        CREATE: "created the profile {this.profile.username} on {this.service.name}",
+    }
+
+    user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
+    profile_uuid = db.Column(db.String(36), db.ForeignKey(Profile.uuid))
+    service_uuid = db.Column(db.String(36), db.ForeignKey(Service.uuid))
+    actor_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
+    user = db.relationship(User, foreign_keys=[user_uuid],
+        backref=db.backref('history', cascade='all, delete-orphan'))
+    profile = db.relationship(Profile,
+        backref=db.backref('history', cascade='all, delete-orphan'))
+    service = db.relationship(Service,
+        backref=db.backref('history', cascade='all, delete-orphan'))
+    actor = db.relationship(User, foreign_keys=[actor_uuid],
+        backref=db.backref('actions', cascade='all, delete-orphan'))
+
+    category = db.Column(db.String(25))
+    value = db.Column(db.String())
+
+    @property
+    def description(self):
+        print(History.DESCRIPTION.get(self.category, "").format(this=self))
+        return History.DESCRIPTION.get(self.category, "").format(this=self)
diff --git a/trurt/templates/base.html b/trurt/templates/base.html
index 68eb448..3b7b8f3 100644
--- a/trurt/templates/base.html
+++ b/trurt/templates/base.html
@@ -1,3 +1,4 @@
+{% import "macros.html" as macros %}
 <!doctype html>
 <html>
   <head>
diff --git a/trurt/templates/macros.html b/trurt/templates/macros.html
new file mode 100644
index 0000000..60b77a1
--- /dev/null
+++ b/trurt/templates/macros.html
@@ -0,0 +1,36 @@
+{% macro timeline(events) %}
+<ul class="timeline">
+  {% set dates = [] %}
+  {% for event in events | reverse %}
+  {% if not event.created_at.date() == dates[-1] %}
+  {% set _ = dates.append(event.created_at.date()) %}
+  <li class="time-label">
+    <span class="bg-red">{{ event.created_at.date() }}</span>
+  </li>
+  {% endif %}
+  <li>
+    <i class"fa fa-enveloppe bg-blue"></i>
+    <div class="timeline-item">
+      <span class="time"><i class="fa fa-clock-o"></i> {{ event.created_at.time().strftime("%H:%m") }}</span>
+      <h3 class="timeline-header">
+        <strong>{{ event.actor.username or "You" }}</strong>
+        {{ event.description }}
+      </h3>
+      <div class="timeline-body">
+        {{ event.comment }}
+      </div>
+    </div>
+  </li>
+  {% endfor %}
+</ul>
+{% endmacro %}
+
+{% macro infobox(title, text, color, icon) %}
+<div class="info-box">
+  <span class="info-box-icon bg-{{ color }}"><i class="fa fa-{{ icon }}"></i></span>
+  <div class="info-box-content">
+    <span class="info-box-text">{{ title }}</span>
+    <span class="info-box-number">{{ text }}</span>
+  </div>
+</div>
+{% endmacro %}
-- 
GitLab