From 13b5fd2a25aa4a3134ff2a32bd4a4efedfba4384 Mon Sep 17 00:00:00 2001 From: Ivan Fonseca Date: Wed, 20 Feb 2019 03:20:38 -0500 Subject: [PATCH] Allow users to write a bio to show on user page --- .../3f83028d1673_add_user_bio_column.py | 30 +++++++++++++++++ tildes/tildes/models/user/user.py | 33 ++++++++++++++++++- tildes/tildes/routes.py | 1 + tildes/tildes/schemas/user.py | 16 +++++++++ tildes/tildes/templates/settings.jinja2 | 5 +++ tildes/tildes/templates/settings_bio.jinja2 | 25 ++++++++++++++ tildes/tildes/templates/user.jinja2 | 7 ++++ tildes/tildes/views/api/web/user.py | 16 +++++++++ tildes/tildes/views/settings.py | 7 ++++ 9 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 tildes/alembic/versions/3f83028d1673_add_user_bio_column.py create mode 100644 tildes/tildes/templates/settings_bio.jinja2 diff --git a/tildes/alembic/versions/3f83028d1673_add_user_bio_column.py b/tildes/alembic/versions/3f83028d1673_add_user_bio_column.py new file mode 100644 index 0000000..eac0c99 --- /dev/null +++ b/tildes/alembic/versions/3f83028d1673_add_user_bio_column.py @@ -0,0 +1,30 @@ +"""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") diff --git a/tildes/tildes/models/user/user.py b/tildes/tildes/models/user/user.py index 4ea7e20..0c575b2 100644 --- a/tildes/tildes/models/user/user.py +++ b/tildes/tildes/models/user/user.py @@ -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"" diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index 7b1550a..64b12c9 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -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 ) diff --git a/tildes/tildes/schemas/user.py b/tildes/tildes/schemas/user.py index 5adc2d0..3b27284 100644 --- a/tildes/tildes/schemas/user.py +++ b/tildes/tildes/schemas/user.py @@ -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.""" diff --git a/tildes/tildes/templates/settings.jinja2 b/tildes/tildes/templates/settings.jinja2 index 9abf1e0..4107bcb 100644 --- a/tildes/tildes/templates/settings.jinja2 +++ b/tildes/tildes/templates/settings.jinja2 @@ -130,5 +130,10 @@
  • Define topic tag filters
    Define a list of topic tags to filter out of listings by default
    +
  • +
  • + Edit your user bio +
    Tell others about yourself with a short bio on your user page
    +
  • {% endblock %} diff --git a/tildes/tildes/templates/settings_bio.jinja2 b/tildes/tildes/templates/settings_bio.jinja2 new file mode 100644 index 0000000..556c889 --- /dev/null +++ b/tildes/tildes/templates/settings_bio.jinja2 @@ -0,0 +1,25 @@ +{# Copyright (c) 2019 Tildes contributors #} +{# 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 %} +
    + {{ markdown_textarea('User Bio (Markdown)', text=request.user.bio_markdown) }} + +
    + +
    +
    +{% endblock %} diff --git a/tildes/tildes/templates/user.jinja2 b/tildes/tildes/templates/user.jinja2 index 3edddbd..9a75569 100644 --- a/tildes/tildes/templates/user.jinja2 +++ b/tildes/tildes/templates/user.jinja2 @@ -120,6 +120,13 @@
    Registered
    {{ user.created_time.strftime('%B %-d, %Y') }}
    + + {% if user.bio_rendered_html %} +
    +
    Bio
    +
    {{ user.bio_rendered_html|safe }}
    +
    + {% endif %}
    {% if request.has_permission('message', user) %} diff --git a/tildes/tildes/views/api/web/user.py b/tildes/tildes/views/api/web/user.py index 8cd8483..e071e0d 100644 --- a/tildes/tildes/views/api/web/user.py +++ b/tildes/tildes/views/api/web/user.py @@ -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", diff --git a/tildes/tildes/views/settings.py b/tildes/tildes/views/settings.py index 9b44fe1..05ab15f 100644 --- a/tildes/tildes/views/settings.py +++ b/tildes/tildes/views/settings.py @@ -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( {