Commit 13b5fd2a authored by Ivan Fonseca's avatar Ivan Fonseca 🎵 Committed by Deimos

Allow users to write a bio to show on user page

parent 18dabdfe
"""Add user bio column
Revision ID: 3f83028d1673
Revises: 4ebc3ca32b48
Create Date: 2019-02-20 08:17:49.636855
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "3f83028d1673"
down_revision = "4ebc3ca32b48"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("users", sa.Column("bio_markdown", sa.Text(), nullable=True))
op.add_column("users", sa.Column("bio_rendered_html", sa.Text(), nullable=True))
op.create_check_constraint(
"bio_markdown_length", "users", "LENGTH(bio_markdown) <= 2000"
)
def downgrade():
op.drop_constraint("ck_users_bio_markdown_length", "users")
op.drop_column("users", "bio_rendered_html")
op.drop_column("users", "bio_markdown")
......@@ -36,8 +36,13 @@ from tildes.enums import CommentLabelOption, TopicSortOption
from tildes.lib.database import ArrayOfLtree, CIText
from tildes.lib.datetime import utc_now
from tildes.lib.hash import hash_string, is_match_for_hash
from tildes.lib.markdown import convert_markdown_to_safe_html
from tildes.models import DatabaseModel
from tildes.schemas.user import EMAIL_ADDRESS_NOTE_MAX_LENGTH, UserSchema
from tildes.schemas.user import (
BIO_MAX_LENGTH,
EMAIL_ADDRESS_NOTE_MAX_LENGTH,
UserSchema,
)
class User(DatabaseModel):
......@@ -111,6 +116,14 @@ class User(DatabaseModel):
)
comment_label_weight: Optional[float] = Column(REAL)
last_exemplary_label_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
_bio_markdown: str = Column(
"bio_markdown",
Text,
CheckConstraint(
f"LENGTH(bio_markdown) <= {BIO_MAX_LENGTH}", name="bio_markdown_length"
),
)
bio_rendered_html: str = Column(Text)
@hybrid_property
def filtered_topic_tags(self) -> List[str]:
......@@ -121,6 +134,24 @@ class User(DatabaseModel):
def filtered_topic_tags(self, new_tags: List[str]) -> None:
self._filtered_topic_tags = new_tags
@hybrid_property
def bio_markdown(self) -> str:
"""Return the user bio's markdown."""
return self._bio_markdown
@bio_markdown.setter # type: ignore
def bio_markdown(self, new_markdown: str) -> None:
"""Set the user bio's markdown and render its HTML."""
if new_markdown == self.bio_markdown:
return
self._bio_markdown = new_markdown
if self._bio_markdown is not None:
self.bio_rendered_html = convert_markdown_to_safe_html(new_markdown)
else:
self.bio_rendered_html = None
def __repr__(self) -> str:
"""Display the user's username and ID as its repr format."""
return f"<User {self.username} ({self.user_id})>"
......
......@@ -72,6 +72,7 @@ def includeme(config: Configurator) -> None:
"settings_comment_visits", "/comment_visits", factory=LoggedInFactory
)
config.add_route("settings_filters", "/filters", factory=LoggedInFactory)
config.add_route("settings_bio", "/bio", factory=LoggedInFactory)
config.add_route(
"settings_password_change", "/password_change", factory=LoggedInFactory
)
......
......@@ -11,6 +11,7 @@ from marshmallow.fields import Boolean, DateTime, Email, String
from marshmallow.validate import Length, Regexp
from tildes.lib.password import is_breached_password
from tildes.schemas.fields import Markdown
USERNAME_MIN_LENGTH = 3
......@@ -36,6 +37,8 @@ PASSWORD_MIN_LENGTH = 8
EMAIL_ADDRESS_NOTE_MAX_LENGTH = 100
BIO_MAX_LENGTH = 2000
class UserSchema(Schema):
"""Marshmallow schema for users."""
......@@ -54,6 +57,7 @@ class UserSchema(Schema):
email_address_note = String(validate=Length(max=EMAIL_ADDRESS_NOTE_MAX_LENGTH))
created_time = DateTime(dump_only=True)
track_comment_visits = Boolean()
bio_markdown = Markdown(max_length=BIO_MAX_LENGTH, allow_none=True)
@post_dump
def anonymize_username(self, data: dict) -> dict:
......@@ -123,6 +127,18 @@ class UserSchema(Schema):
return data
@pre_load
def prepare_bio_markdown(self, data: dict) -> dict:
"""Prepare the bio_markdown value before it's validated."""
if "bio_markdown" not in data:
return data
# if the value is empty, convert it to None
if not data["bio_markdown"] or data["bio_markdown"].isspace():
data["bio_markdown"] = None
return data
class Meta:
"""Always use strict checking so error handlers are invoked."""
......
......@@ -130,5 +130,10 @@
<li>
<a href="/settings/filters">Define topic tag filters</a>
<div class="text-small text-secondary">Define a list of topic tags to filter out of listings by default</div>
</li>
<li>
<a href="/settings/bio">Edit your user bio</a>
<div class="text-small text-secondary">Tell others about yourself with a short bio on your user page</div>
</li>
</ul>
{% endblock %}
{# Copyright (c) 2019 Tildes contributors <code@tildes.net> #}
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{% extends 'base_no_sidebar.jinja2' %}
{% from 'macros/forms.jinja2' import markdown_textarea %}
{% block title %}Edit your user bio{% endblock %}
{% block main_heading %}Edit your user bio{% endblock %}
{% block content %}
<form
method="post"
name="user-bio"
autocomplete="off"
data-ic-patch-to="{{ request.route_url("ic_user", username=request.user.username) }}"
>
{{ markdown_textarea('User Bio (Markdown)', text=request.user.bio_markdown) }}
<div class="form-buttons">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
{% endblock %}
......@@ -120,6 +120,13 @@
<dl>
<dt>Registered</dt>
<dd>{{ user.created_time.strftime('%B %-d, %Y') }}</dd>
{% if user.bio_rendered_html %}
<div class="user-bio">
<dt>Bio</dt>
<dd>{{ user.bio_rendered_html|safe }}</dd>
</div>
{% endif %}
</dl>
{% if request.has_permission('message', user) %}
......
......@@ -228,6 +228,22 @@ def patch_change_account_default_theme(request: Request) -> Response:
return IC_NOOP
@ic_view_config(
route_name="user",
request_method="PATCH",
request_param="ic-trigger-name=user-bio",
permission="edit_bio",
)
@use_kwargs({"markdown": String()})
def patch_change_user_bio(request: Request, markdown: str) -> dict:
"""Update a user's bio."""
user = request.context
user.bio_markdown = markdown
return IC_NOOP
@ic_view_config(
route_name="user_invite_code",
request_method="GET",
......
......@@ -109,6 +109,13 @@ def get_settings_two_factor_qr_code(request: Request) -> Response:
return Response(byte_io.getvalue(), cache_control="private, no-cache")
@view_config(route_name="settings_bio", renderer="settings_bio.jinja2")
def get_settings_bio(request: Request) -> dict:
"""Generate the user bio settings page."""
# pylint: disable=unused-argument
return {}
@view_config(route_name="settings_password_change", request_method="POST")
@use_kwargs(
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment