Add feature to use Real name on journals
This commit is contained in:
@@ -39,7 +39,7 @@ After it prints `ADMIN_PASSWORD_ROTATED_MFA_RESET`, sign in as `admin` with the
|
|||||||
|
|
||||||
## Email and password recovery
|
## Email and password recovery
|
||||||
|
|
||||||
Registration now requires an email address and a confirmation link before the account can sign in. Members can change their email address from the navigation; the current address remains active until the link sent to the new address is confirmed. Verification and password-reset links expire after 24 hours and one hour respectively, and can only be used once. Administrators can send the same recovery email to any approved member from **Accounts**.
|
Registration requires a real name, email address, and confirmation link before the account can sign in. The real name is used as the published-by name, while existing accounts continue to use their handle until they set a profile name. Members can change their email address from the navigation; the current address remains active until the link sent to the new address is confirmed. Verification and password-reset links expire after 24 hours and one hour respectively, and can only be used once. Administrators can send the same recovery email to any approved member from **Accounts**.
|
||||||
|
|
||||||
All browser POST forms are protected by server-validated CSRF tokens.
|
All browser POST forms are protected by server-validated CSRF tokens.
|
||||||
|
|
||||||
|
|||||||
@@ -14,5 +14,5 @@
|
|||||||
- [ ] Add code snippets to long-form stories.
|
- [ ] Add code snippets to long-form stories.
|
||||||
- [x] Allow users to change their email addresses.
|
- [x] Allow users to change their email addresses.
|
||||||
- [x] Add user email verification for registration and email-address changes.
|
- [x] Add user email verification for registration and email-address changes.
|
||||||
- [ ] Add user real names and use them as the published-by name.
|
- [x] Add user real names and use them as the published-by name.
|
||||||
- [ ] Add ability to hide posts from unregistered users.
|
- [ ] Add ability to hide posts from unregistered users.
|
||||||
@@ -121,6 +121,7 @@ def init_db():
|
|||||||
"mfa_enabled": "ALTER TABLE users ADD COLUMN mfa_enabled INTEGER NOT NULL DEFAULT 0",
|
"mfa_enabled": "ALTER TABLE users ADD COLUMN mfa_enabled INTEGER NOT NULL DEFAULT 0",
|
||||||
"email": "ALTER TABLE users ADD COLUMN email TEXT",
|
"email": "ALTER TABLE users ADD COLUMN email TEXT",
|
||||||
"email_verified": "ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0",
|
"email_verified": "ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0",
|
||||||
|
"real_name": "ALTER TABLE users ADD COLUMN real_name TEXT",
|
||||||
}
|
}
|
||||||
post_columns = {row["name"] for row in db.execute("PRAGMA table_info(posts)")}
|
post_columns = {row["name"] for row in db.execute("PRAGMA table_info(posts)")}
|
||||||
for column, statement in migrations.items():
|
for column, statement in migrations.items():
|
||||||
@@ -252,6 +253,14 @@ def valid_email(email):
|
|||||||
return re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", email) is not None
|
return re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", email) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_real_name(real_name):
|
||||||
|
return " ".join(real_name.split())
|
||||||
|
|
||||||
|
|
||||||
|
def valid_real_name(real_name):
|
||||||
|
return 2 <= len(real_name) <= 100 and all(character.isalnum() or character in " .'-" for character in real_name)
|
||||||
|
|
||||||
|
|
||||||
def create_reset_token(user_id):
|
def create_reset_token(user_id):
|
||||||
token = token_urlsafe(32)
|
token = token_urlsafe(32)
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -321,14 +330,14 @@ def send_verification_email(user, email_address, token_type):
|
|||||||
def inject_current_user():
|
def inject_current_user():
|
||||||
user = None
|
user = None
|
||||||
if "user_id" in session:
|
if "user_id" in session:
|
||||||
user = get_db().execute("SELECT id, username, role FROM users WHERE id = ?", (session["user_id"],)).fetchone()
|
user = get_db().execute("SELECT id, username, real_name, role FROM users WHERE id = ?", (session["user_id"],)).fetchone()
|
||||||
return {"current_user": user}
|
return {"current_user": user}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
posts = get_db().execute(
|
posts = get_db().execute(
|
||||||
"""SELECT posts.*, users.username FROM posts JOIN users ON users.id = posts.author_id
|
"""SELECT posts.*, users.username, users.real_name FROM posts JOIN users ON users.id = posts.author_id
|
||||||
ORDER BY posts.created_at DESC"""
|
ORDER BY posts.created_at DESC"""
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return render_template("index.html", posts=posts)
|
return render_template("index.html", posts=posts)
|
||||||
@@ -337,7 +346,7 @@ def index():
|
|||||||
@app.route("/post/<slug>")
|
@app.route("/post/<slug>")
|
||||||
def post(slug):
|
def post(slug):
|
||||||
article = get_db().execute(
|
article = get_db().execute(
|
||||||
"""SELECT posts.*, users.username FROM posts JOIN users ON users.id = posts.author_id
|
"""SELECT posts.*, users.username, users.real_name FROM posts JOIN users ON users.id = posts.author_id
|
||||||
WHERE posts.slug = ?""",
|
WHERE posts.slug = ?""",
|
||||||
(slug,),
|
(slug,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
@@ -356,10 +365,13 @@ def uploaded_image(filename):
|
|||||||
def register():
|
def register():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
username = request.form.get("username", "").strip().lower()
|
username = request.form.get("username", "").strip().lower()
|
||||||
|
real_name = normalize_real_name(request.form.get("real_name", ""))
|
||||||
email = request.form.get("email", "").strip().lower()
|
email = request.form.get("email", "").strip().lower()
|
||||||
password = request.form.get("password", "")
|
password = request.form.get("password", "")
|
||||||
if not re.fullmatch(r"[a-z0-9_-]{3,32}", username):
|
if not re.fullmatch(r"[a-z0-9_-]{3,32}", username):
|
||||||
flash("Use 3-32 lowercase letters, numbers, hyphens, or underscores.", "error")
|
flash("Use 3-32 lowercase letters, numbers, hyphens, or underscores.", "error")
|
||||||
|
elif not valid_real_name(real_name):
|
||||||
|
flash("Enter a real name using 2-100 letters, numbers, spaces, hyphens, apostrophes, or periods.", "error")
|
||||||
elif len(password) < 10:
|
elif len(password) < 10:
|
||||||
flash("Choose a password with at least 10 characters.", "error")
|
flash("Choose a password with at least 10 characters.", "error")
|
||||||
elif not valid_email(email):
|
elif not valid_email(email):
|
||||||
@@ -367,8 +379,8 @@ def register():
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
cursor = get_db().execute(
|
cursor = get_db().execute(
|
||||||
"INSERT INTO users (username, email, password_hash, created_at, is_approved) VALUES (?, ?, ?, ?, 0)",
|
"INSERT INTO users (username, real_name, email, password_hash, created_at, is_approved) VALUES (?, ?, ?, ?, ?, 0)",
|
||||||
(username, email, generate_password_hash(password), datetime.now(timezone.utc).isoformat()),
|
(username, real_name, email, generate_password_hash(password), datetime.now(timezone.utc).isoformat()),
|
||||||
)
|
)
|
||||||
get_db().commit()
|
get_db().commit()
|
||||||
user = {"id": cursor.lastrowid, "username": username}
|
user = {"id": cursor.lastrowid, "username": username}
|
||||||
@@ -509,6 +521,22 @@ def change_password():
|
|||||||
return render_template("password_form.html", mode="change")
|
return render_template("password_form.html", mode="change")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/account/profile", methods=("GET", "POST"))
|
||||||
|
@login_required
|
||||||
|
def change_profile():
|
||||||
|
user = get_db().execute("SELECT real_name FROM users WHERE id = ?", (session["user_id"],)).fetchone()
|
||||||
|
if request.method == "POST":
|
||||||
|
real_name = normalize_real_name(request.form.get("real_name", ""))
|
||||||
|
if not valid_real_name(real_name):
|
||||||
|
flash("Enter a real name using 2-100 letters, numbers, spaces, hyphens, apostrophes, or periods.", "error")
|
||||||
|
else:
|
||||||
|
get_db().execute("UPDATE users SET real_name = ? WHERE id = ?", (real_name, session["user_id"]))
|
||||||
|
get_db().commit()
|
||||||
|
flash("Profile updated.", "success")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
return render_template("profile_form.html", real_name=user["real_name"] or "")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/account/email", methods=("GET", "POST"))
|
@app.route("/account/email", methods=("GET", "POST"))
|
||||||
@login_required
|
@login_required
|
||||||
def change_email():
|
def change_email():
|
||||||
@@ -599,7 +627,7 @@ def mfa_verify():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def admin_users():
|
def admin_users():
|
||||||
users = get_db().execute(
|
users = get_db().execute(
|
||||||
"SELECT id, username, email, email_verified, created_at, is_approved, mfa_enabled, role FROM users ORDER BY is_approved, created_at DESC"
|
"SELECT id, username, real_name, email, email_verified, created_at, is_approved, mfa_enabled, role FROM users ORDER BY is_approved, created_at DESC"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return render_template("admin_users.html", users=users)
|
return render_template("admin_users.html", users=users)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -7,7 +7,7 @@
|
|||||||
<div class="account-row account-head" role="row"><span>HANDLE / EMAIL</span><span>REGISTERED</span><span>MFA</span><span>STATUS</span></div>
|
<div class="account-row account-head" role="row"><span>HANDLE / EMAIL</span><span>REGISTERED</span><span>MFA</span><span>STATUS</span></div>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<div class="account-row" role="row">
|
<div class="account-row" role="row">
|
||||||
<strong>{{ user.username }}{% if user.role == 'admin' %} <small>ADMIN</small>{% endif %}<small class="email">{{ user.email or 'NO EMAIL' }}{% if user.email %} / {{ 'VERIFIED' if user.email_verified else 'UNVERIFIED' }}{% endif %}</small></strong>
|
<strong>{{ user.real_name or user.username }}{% if user.role == 'admin' %} <small>ADMIN</small>{% endif %}<small class="email">@{{ user.username }} / {{ user.email or 'NO EMAIL' }}{% if user.email %} / {{ 'VERIFIED' if user.email_verified else 'UNVERIFIED' }}{% endif %}</small></strong>
|
||||||
<span>{{ user.created_at[:10] }}</span>
|
<span>{{ user.created_at[:10] }}</span>
|
||||||
<span>{{ 'ENABLED' if user.mfa_enabled else 'PENDING' }}</span>
|
<span>{{ 'ENABLED' if user.mfa_enabled else 'PENDING' }}</span>
|
||||||
<span>{% if user.is_approved and user.role == 'member' %}<form method="post" action="{{ url_for('admin_password_reset', user_id=user.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="button compact" type="submit">Reset password</button></form>{% elif user.is_approved %}APPROVED{% else %}<form method="post" action="{{ url_for('approve_user', user_id=user.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="button compact" type="submit">Approve</button></form>{% endif %}</span>
|
<span>{% if user.is_approved and user.role == 'member' %}<form method="post" action="{{ url_for('admin_password_reset', user_id=user.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="button compact" type="submit">Reset password</button></form>{% elif user.is_approved %}APPROVED{% else %}<form method="post" action="{{ url_for('approve_user', user_id=user.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="button compact" type="submit">Approve</button></form>{% endif %}</span>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<form class="auth-form" method="post">
|
<form class="auth-form" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<label>Handle<input name="username" autocomplete="username" required pattern="[a-z0-9_-]{3,32}" placeholder="your-handle"></label>
|
<label>Handle<input name="username" autocomplete="username" required pattern="[a-z0-9_-]{3,32}" placeholder="your-handle"></label>
|
||||||
|
{% if mode == 'register' %}<label>Real name<input name="real_name" autocomplete="name" required minlength="2" maxlength="100" placeholder="Your name"></label>{% endif %}
|
||||||
{% if mode == 'register' %}<label>Email<input name="email" type="email" autocomplete="email" required placeholder="you@example.com"></label>{% endif %}
|
{% if mode == 'register' %}<label>Email<input name="email" type="email" autocomplete="email" required placeholder="you@example.com"></label>{% endif %}
|
||||||
<label>Password<input name="password" type="password" autocomplete="{{ 'new-password' if mode == 'register' else 'current-password' }}" required {% if mode == 'register' %}minlength="10" placeholder="10+ characters"{% else %}placeholder="Your password"{% endif %}></label>
|
<label>Password<input name="password" type="password" autocomplete="{{ 'new-password' if mode == 'register' else 'current-password' }}" required {% if mode == 'register' %}minlength="10" placeholder="10+ characters"{% else %}placeholder="Your password"{% endif %}></label>
|
||||||
<button class="button" type="submit">{% if mode == 'register' %}Create account{% elif mode == 'resend' %}Resend verification{% else %}Sign in{% endif %} <span aria-hidden="true">→</span></button>
|
<button class="button" type="submit">{% if mode == 'register' %}Create account{% elif mode == 'resend' %}Resend verification{% else %}Sign in{% endif %} <span aria-hidden="true">→</span></button>
|
||||||
|
|||||||
+2
-1
@@ -21,9 +21,10 @@
|
|||||||
{% if current_user %}
|
{% if current_user %}
|
||||||
<a href="{{ url_for('write') }}">Write</a>
|
<a href="{{ url_for('write') }}">Write</a>
|
||||||
{% if current_user.role == 'admin' %}<a href="{{ url_for('admin_users') }}">Accounts</a>{% endif %}
|
{% if current_user.role == 'admin' %}<a href="{{ url_for('admin_users') }}">Accounts</a>{% endif %}
|
||||||
|
<a href="{{ url_for('change_profile') }}">Profile</a>
|
||||||
<a href="{{ url_for('change_email') }}">Email</a>
|
<a href="{{ url_for('change_email') }}">Email</a>
|
||||||
<a href="{{ url_for('change_password') }}">Password</a>
|
<a href="{{ url_for('change_password') }}">Password</a>
|
||||||
<span class="user-chip">{{ current_user.username }}</span>
|
<span class="user-chip">{{ current_user.real_name or current_user.username }}</span>
|
||||||
<form action="{{ url_for('logout') }}" method="post"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="text-button" type="submit">Log out</button></form>
|
<form action="{{ url_for('logout') }}" method="post"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="text-button" type="submit">Log out</button></form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('login') }}">Sign in</a>
|
<a href="{{ url_for('login') }}">Sign in</a>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="post-meta"><span>{{ post.category }}</span><time datetime="{{ post.created_at }}">{{ post.created_at[:10] }}</time></div>
|
<div class="post-meta"><span>{{ post.category }}</span><time datetime="{{ post.created_at }}">{{ post.created_at[:10] }}</time></div>
|
||||||
<h2><a href="{{ url_for('post', slug=post.slug) }}">{{ post.title }}</a></h2>
|
<h2><a href="{{ url_for('post', slug=post.slug) }}">{{ post.title }}</a></h2>
|
||||||
<p>{{ post.excerpt }}</p>
|
<p>{{ post.excerpt }}</p>
|
||||||
<div class="post-foot"><span>BY {{ post.username }}</span><a href="{{ url_for('post', slug=post.slug) }}" aria-label="Read {{ post.title }}">→</a></div>
|
<div class="post-foot"><span>BY {{ post.real_name or post.username }}</span><a href="{{ url_for('post', slug=post.slug) }}" aria-label="Read {{ post.title }}">→</a></div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}{{ post.title }} | Eternity Project{% endblock %}
|
{% block title %}{{ post.title }} | Eternity Project{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article class="article"><div class="post-meta"><span>{{ post.category }}</span><time datetime="{{ post.created_at }}">{{ post.created_at[:10] }}</time></div><h1>{{ post.title }}</h1><p class="article-lead">{{ post.excerpt }}</p><div class="byline">WRITTEN BY {{ post.username }}{% if post.updated_at != post.created_at %} / UPDATED {{ post.updated_at[:10] }}{% endif %}</div>{% if post.image_filename %}<button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=post.image_filename) }}" data-alt="Image for {{ post.title }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=post.image_filename) }}" alt="Image for {{ post.title }}"></button>{% endif %}<div class="article-body">{% for kind, content in blocks %}{% if kind == 'text' %}<div class="article-text">{{ content }}</div>{% else %}<figure class="inline-image"><button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=content.filename) }}" data-alt="{{ content.caption or 'Article step image' }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=content.filename) }}" alt="{{ content.caption or 'Article step image' }}"></button>{% if content.caption %}<figcaption>{{ content.caption }}</figcaption>{% endif %}</figure>{% endif %}{% endfor %}</div>{% if current_user and current_user.id == post.author_id %}<a class="button compact" href="{{ url_for('edit_post', slug=post.slug) }}">Edit post</a>{% endif %}</article>
|
<article class="article"><div class="post-meta"><span>{{ post.category }}</span><time datetime="{{ post.created_at }}">{{ post.created_at[:10] }}</time></div><h1>{{ post.title }}</h1><p class="article-lead">{{ post.excerpt }}</p><div class="byline">WRITTEN BY {{ post.real_name or post.username }}{% if post.updated_at != post.created_at %} / UPDATED {{ post.updated_at[:10] }}{% endif %}</div>{% if post.image_filename %}<button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=post.image_filename) }}" data-alt="Image for {{ post.title }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=post.image_filename) }}" alt="Image for {{ post.title }}"></button>{% endif %}<div class="article-body">{% for kind, content in blocks %}{% if kind == 'text' %}<div class="article-text">{{ content }}</div>{% else %}<figure class="inline-image"><button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=content.filename) }}" data-alt="{{ content.caption or 'Article step image' }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=content.filename) }}" alt="{{ content.caption or 'Article step image' }}"></button>{% if content.caption %}<figcaption>{{ content.caption }}</figcaption>{% endif %}</figure>{% endif %}{% endfor %}</div>{% if current_user and current_user.id == post.author_id %}<a class="button compact" href="{{ url_for('edit_post', slug=post.slug) }}">Edit post</a>{% endif %}</article>
|
||||||
<dialog class="image-dialog"><button class="dialog-close" type="button" aria-label="Close full-size image">Close</button><img alt=""></dialog>
|
<dialog class="image-dialog"><button class="dialog-close" type="button" aria-label="Close full-size image">Close</button><img alt=""></dialog>
|
||||||
<script>const imageDialog=document.querySelector('.image-dialog'),dialogImage=imageDialog.querySelector('img');document.querySelectorAll('.article-image-button').forEach(button=>button.addEventListener('click',()=>{dialogImage.src=button.dataset.image;dialogImage.alt=button.dataset.alt;imageDialog.showModal()}));imageDialog.querySelector('.dialog-close').addEventListener('click',()=>imageDialog.close());imageDialog.addEventListener('click',event=>{if(event.target===imageDialog)imageDialog.close()});</script>
|
<script>const imageDialog=document.querySelector('.image-dialog'),dialogImage=imageDialog.querySelector('img');document.querySelectorAll('.article-image-button').forEach(button=>button.addEventListener('click',()=>{dialogImage.src=button.dataset.image;dialogImage.alt=button.dataset.alt;imageDialog.showModal()}));imageDialog.querySelector('.dialog-close').addEventListener('click',()=>imageDialog.close());imageDialog.addEventListener('click',event=>{if(event.target===imageDialog)imageDialog.close()});</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Profile | Eternity Project{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="auth-layout">
|
||||||
|
<div class="auth-copy"><span class="signal-label">PROFILE</span><h1>Set your<br>byline.</h1><p>Your real name appears as the published-by name on your posts.</p></div>
|
||||||
|
<form class="auth-form" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<label>Real name<input name="real_name" value="{{ real_name }}" autocomplete="name" required minlength="2" maxlength="100" placeholder="Your name"></label>
|
||||||
|
<button class="button" type="submit">Save profile <span aria-hidden="true">→</span></button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -37,7 +37,7 @@ class EmailVerificationTestCase(unittest.TestCase):
|
|||||||
with patch("app.smtplib.SMTP") as smtp:
|
with patch("app.smtplib.SMTP") as smtp:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/register",
|
"/register",
|
||||||
data={"username": "newmember", "email": "new@example.com", "password": "secure-password"},
|
data={"username": "newmember", "real_name": "New Member", "email": "new@example.com", "password": "secure-password"},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
@@ -85,6 +85,40 @@ class EmailVerificationTestCase(unittest.TestCase):
|
|||||||
with application.app.app_context():
|
with application.app.app_context():
|
||||||
self.assertEqual(application.get_db().execute("SELECT email FROM users WHERE id = ?", (user_id,)).fetchone()[0], "new@example.com")
|
self.assertEqual(application.get_db().execute("SELECT email FROM users WHERE id = ?", (user_id,)).fetchone()[0], "new@example.com")
|
||||||
|
|
||||||
|
def test_profile_name_is_used_for_byline_with_handle_fallback(self):
|
||||||
|
with application.app.app_context():
|
||||||
|
db = application.get_db()
|
||||||
|
named_author = db.execute(
|
||||||
|
"""INSERT INTO users (username, email, email_verified, password_hash, created_at, is_approved, role, mfa_enabled)
|
||||||
|
VALUES (?, ?, 1, ?, ?, 1, 'member', 1)""",
|
||||||
|
("named", "named@example.com", generate_password_hash("secure-password"), "2026-09-01T00:00:00+00:00"),
|
||||||
|
).lastrowid
|
||||||
|
legacy_author = db.execute(
|
||||||
|
"""INSERT INTO users (username, email, email_verified, password_hash, created_at, is_approved, role, mfa_enabled)
|
||||||
|
VALUES (?, ?, 1, ?, ?, 1, 'member', 1)""",
|
||||||
|
("legacy", "legacy@example.com", generate_password_hash("secure-password"), "2026-09-01T00:00:00+00:00"),
|
||||||
|
).lastrowid
|
||||||
|
db.execute(
|
||||||
|
"""INSERT INTO posts (title, slug, excerpt, body, category, author_id, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
|
("Named post", "named-post", "Named excerpt", "Named body", "Code", named_author, "2026-09-01T00:00:00+00:00", "2026-09-01T00:00:00+00:00"),
|
||||||
|
)
|
||||||
|
db.execute(
|
||||||
|
"""INSERT INTO posts (title, slug, excerpt, body, category, author_id, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
|
("Legacy post", "legacy-post", "Legacy excerpt", "Legacy body", "Code", legacy_author, "2026-09-01T00:00:00+00:00", "2026-09-01T00:00:00+00:00"),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
with self.client.session_transaction() as session:
|
||||||
|
session["user_id"] = named_author
|
||||||
|
session["mfa_verified"] = True
|
||||||
|
|
||||||
|
response = self.client.post("/account/profile", data={"real_name": "Ada Lovelace"})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertIn(b"BY Ada Lovelace", self.client.get("/").data)
|
||||||
|
self.assertIn(b"WRITTEN BY Ada Lovelace", self.client.get("/post/named-post").data)
|
||||||
|
self.assertIn(b"BY legacy", self.client.get("/").data)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in New Issue
Block a user