Browse Source

Allow users to write a bio to show on user page

merge-requests/64/head
Ivan Fonseca 6 years ago
committed by Deimos
parent
commit
13b5fd2a25
  1. 30
      tildes/alembic/versions/3f83028d1673_add_user_bio_column.py
  2. 33
      tildes/tildes/models/user/user.py
  3. 1
      tildes/tildes/routes.py
  4. 16
      tildes/tildes/schemas/user.py
  5. 5
      tildes/tildes/templates/settings.jinja2
  6. 25
      tildes/tildes/templates/settings_bio.jinja2
  7. 7
      tildes/tildes/templates/user.jinja2
  8. 16
      tildes/tildes/views/api/web/user.py
  9. 7
      tildes/tildes/views/settings.py

30
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")

33
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.database import ArrayOfLtree, CIText
from tildes.lib.datetime import utc_now from tildes.lib.datetime import utc_now
from tildes.lib.hash import hash_string, is_match_for_hash 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.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): class User(DatabaseModel):
@ -111,6 +116,14 @@ class User(DatabaseModel):
) )
comment_label_weight: Optional[float] = Column(REAL) comment_label_weight: Optional[float] = Column(REAL)
last_exemplary_label_time: Optional[datetime] = Column(TIMESTAMP(timezone=True)) 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 @hybrid_property
def filtered_topic_tags(self) -> List[str]: def filtered_topic_tags(self) -> List[str]:
@ -121,6 +134,24 @@ class User(DatabaseModel):
def filtered_topic_tags(self, new_tags: List[str]) -> None: def filtered_topic_tags(self, new_tags: List[str]) -> None:
self._filtered_topic_tags = new_tags 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: def __repr__(self) -> str:
"""Display the user's username and ID as its repr format.""" """Display the user's username and ID as its repr format."""
return f"<User {self.username} ({self.user_id})>" return f"<User {self.username} ({self.user_id})>"

1
tildes/tildes/routes.py

@ -72,6 +72,7 @@ def includeme(config: Configurator) -> None:
"settings_comment_visits", "/comment_visits", factory=LoggedInFactory "settings_comment_visits", "/comment_visits", factory=LoggedInFactory
) )
config.add_route("settings_filters", "/filters", factory=LoggedInFactory) config.add_route("settings_filters", "/filters", factory=LoggedInFactory)
config.add_route("settings_bio", "/bio", factory=LoggedInFactory)
config.add_route( config.add_route(
"settings_password_change", "/password_change", factory=LoggedInFactory "settings_password_change", "/password_change", factory=LoggedInFactory
) )

16
tildes/tildes/schemas/user.py

@ -11,6 +11,7 @@ from marshmallow.fields import Boolean, DateTime, Email, String
from marshmallow.validate import Length, Regexp from marshmallow.validate import Length, Regexp
from tildes.lib.password import is_breached_password from tildes.lib.password import is_breached_password
from tildes.schemas.fields import Markdown
USERNAME_MIN_LENGTH = 3 USERNAME_MIN_LENGTH = 3
@ -36,6 +37,8 @@ PASSWORD_MIN_LENGTH = 8
EMAIL_ADDRESS_NOTE_MAX_LENGTH = 100 EMAIL_ADDRESS_NOTE_MAX_LENGTH = 100
BIO_MAX_LENGTH = 2000
class UserSchema(Schema): class UserSchema(Schema):
"""Marshmallow schema for users.""" """Marshmallow schema for users."""
@ -54,6 +57,7 @@ class UserSchema(Schema):
email_address_note = String(validate=Length(max=EMAIL_ADDRESS_NOTE_MAX_LENGTH)) email_address_note = String(validate=Length(max=EMAIL_ADDRESS_NOTE_MAX_LENGTH))
created_time = DateTime(dump_only=True) created_time = DateTime(dump_only=True)
track_comment_visits = Boolean() track_comment_visits = Boolean()
bio_markdown = Markdown(max_length=BIO_MAX_LENGTH, allow_none=True)
@post_dump @post_dump
def anonymize_username(self, data: dict) -> dict: def anonymize_username(self, data: dict) -> dict:
@ -123,6 +127,18 @@ class UserSchema(Schema):
return data 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: class Meta:
"""Always use strict checking so error handlers are invoked.""" """Always use strict checking so error handlers are invoked."""

5
tildes/tildes/templates/settings.jinja2

@ -130,5 +130,10 @@
<li> <li>
<a href="/settings/filters">Define topic tag filters</a> <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> <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> </ul>
{% endblock %} {% endblock %}

25
tildes/tildes/templates/settings_bio.jinja2

@ -0,0 +1,25 @@
{# 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 %}

7
tildes/tildes/templates/user.jinja2

@ -120,6 +120,13 @@
<dl> <dl>
<dt>Registered</dt> <dt>Registered</dt>
<dd>{{ user.created_time.strftime('%B %-d, %Y') }}</dd> <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> </dl>
{% if request.has_permission('message', user) %} {% if request.has_permission('message', user) %}

16
tildes/tildes/views/api/web/user.py

@ -228,6 +228,22 @@ def patch_change_account_default_theme(request: Request) -> Response:
return IC_NOOP 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( @ic_view_config(
route_name="user_invite_code", route_name="user_invite_code",
request_method="GET", request_method="GET",

7
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") 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") @view_config(route_name="settings_password_change", request_method="POST")
@use_kwargs( @use_kwargs(
{ {

Loading…
Cancel
Save